<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://fzakaria.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://fzakaria.com/" rel="alternate" type="text/html" /><updated>2026-08-02T12:26:51-07:00</updated><id>https://fzakaria.com/feed.xml</id><title type="html">Farid Zakaria’s Blog</title><subtitle>I&apos;m a software engineer, father and wishful amateur surfer. If you&apos;ve come seeking my political views, you&apos;ve found the wrong &lt;a href=&quot;https://fareedzakaria.com/&quot;&gt;Fareed&lt;/a&gt;.</subtitle><entry><title type="html">A C++ toolchain from 357 bytes, in Bazel</title><link href="https://fzakaria.com/2026/08/01/a-c++-toolchain-from-357-bytes-in-bazel" rel="alternate" type="text/html" title="A C++ toolchain from 357 bytes, in Bazel" /><published>2026-08-01T14:10:00-07:00</published><updated>2026-08-01T14:10:00-07:00</updated><id>https://fzakaria.com/2026/08/01/a-c++-toolchain-from-357-bytes-in-bazel</id><content type="html" xml:base="https://fzakaria.com/2026/08/01/a-c++-toolchain-from-357-bytes-in-bazel"><![CDATA[<p>I have been fascinated and amazed by <a href="https://savannah.nongnu.org/projects/stage0/">stage0</a> for a while now ever since I learnt about it via Guix <a href="https://guix.gnu.org/en/blog/2023/the-full-source-bootstrap-building-from-source-all-the-way-down/">using it</a> to provide twenty two thousand packages source-bootstrapped from the 357-byte seed.</p>

<p>What is stage0?</p>

<p>It is a chain of compilers and assemblers that can be built from source, starting from <strong>a 357-byte</strong> program that can eventually build a recent GCC.<sup id="fnref:gcc"><a href="#fn:gcc" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p>Since then, <a href="https://discourse.nixos.org/t/a-full-source-bootstrap-for-nixos/74801">NixOS</a> and <a href="https://github.com/fosslinux/live-bootstrap">other distributions</a> have also adopted the same approach to minimize their binary seed which makes it possible to onboard new architectures and platforms much simpler.</p>

<p>What’s always <em>frustrated</em> me as a <a href="https://bazel.build/">Bazel</a> (&amp; <a href="https://buck2.build/">Buck</a>) user is the reliance on prebuilt toolchains even for things that should be built from source <a href="/2024/11/28/bazel-knowledge-protobuf-is-the-worst-when-it-should-be-the-best">easily like protoc</a>.</p>

<p>Bazel has given up trying to provide a hermetic C++ toolchain and the upstream <a href="https://github.com/bazelbuild/rules_cc">rules_cc</a> ruleset just points you elsewhere:</p>

<blockquote>
  <p>Configuring a hermetic toolchain makes your build more deterministic. rules_cc itself does not yet offer a hermetic toolchain distribution</p>
</blockquote>

<p>I had attempted to provide a stage0 hermetic C++ toolchain in <a href="https://github.com/fzakaria/stage0-bazel/tree/d22d6b050f66a93c1b843c24b8f17dc519dd4802">October 2024</a> via <a href="https://github.com/fzakaria/stage0-bazel">https://github.com/fzakaria/stage0-bazel</a>. I made substantial process through the bootstrap process but I did not make it far enought to be usabale.</p>

<p>To be honest, I was also a little disheartened that no one else in the community thought it was the greatest thing since slice bread. Everyone seems to be content with using prebuilt toolchains as they go deeper into <a href="/2024/07/02/reproducibility-in-disguise">MODULE.bzl madness</a>.</p>

<p>I had put it aside for a while, but I have been thinking about it again recently. The steps are mechanical and the process imitates existing distributions, so this became a perfect project for me to throw at an LLM to finish.<sup id="fnref:llm"><a href="#fn:llm" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p>

<p>You can now leverage the toolchain to build <code class="language-plaintext highlighter-rouge">cc_binary</code> in Bazel and have it compiled by a toolchain whose <strong>entire ancestry</strong> is in the repository from that same <strong>357-byte seed</strong>. 🎆</p>

<p>How complete is this toolchain?</p>

<p>I pointed the toolchain at <a href="https://abseil.io/">Abseil</a> and <a href="https://github.com/google/googletest">GoogleTest</a> straight from the Bazel Central Registry <strong>without any patches</strong>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">bazel_dep</span><span class="p">(</span><span class="n">name</span> <span class="o">=</span> <span class="sh">"</span><span class="s">stage0-bazel</span><span class="sh">"</span><span class="p">,</span> <span class="n">version</span> <span class="o">=</span> <span class="sh">"</span><span class="s">0.1.0</span><span class="sh">"</span><span class="p">)</span>
<span class="nf">bazel_dep</span><span class="p">(</span><span class="n">name</span> <span class="o">=</span> <span class="sh">"</span><span class="s">abseil-cpp</span><span class="sh">"</span><span class="p">,</span> <span class="n">version</span> <span class="o">=</span> <span class="sh">"</span><span class="s">20260107.1</span><span class="sh">"</span><span class="p">)</span>
<span class="nf">bazel_dep</span><span class="p">(</span><span class="n">name</span> <span class="o">=</span> <span class="sh">"</span><span class="s">googletest</span><span class="sh">"</span><span class="p">,</span> <span class="n">version</span> <span class="o">=</span> <span class="sh">"</span><span class="s">1.17.0.bcr.2</span><span class="sh">"</span><span class="p">)</span>


<span class="nf">register_toolchains</span><span class="p">(</span>
    <span class="sh">"</span><span class="s">@stage0-bazel//toolchain:clang</span><span class="sh">"</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">@stage0-bazel//toolchain:cc</span><span class="sh">"</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div>

<p>We then can build and run their testsuite to provide a sanity check that the toolchain is working correctly.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>bazel <span class="nb">test</span> <span class="nt">--target_pattern_file</span><span class="o">=</span>absl-tests.txt
<span class="go">Executed 236 out of 236 tests: 236 tests pass
</span></code></pre></div></div>

<p class="aside">We use a <code class="language-plaintext highlighter-rouge">--target_pattern_file</code> to filter tests that require <code class="language-plaintext highlighter-rouge">google_benchmark</code>. Abseil marks <code class="language-plaintext highlighter-rouge">google_benchmark</code> as a <code class="language-plaintext highlighter-rouge">dev_dependency</code>, and Bzlmod drops dev dependencies of non-root modules.</p>

<p>That is us building Abseil and GoogleTest, from the registry, unpatched, compiled by a toolchain that began as 357 bytes of hex.</p>

<div style="--graphviz-height: 40rem" class="language-graphviz highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">digraph</span> <span class="nv">bootstrap</span> <span class="p">{</span>
  <span class="n">rankdir</span><span class="p">=</span><span class="nv">TB</span><span class="p">;</span>
  <span class="n">fontname</span><span class="p">=</span><span class="s2">"Helvetica"</span><span class="p">;</span>
  <span class="k">node</span> <span class="o">[</span><span class="n">fontname</span><span class="p">=</span><span class="s2">"Helvetica"</span><span class="p">,</span> <span class="n">fontsize</span><span class="p">=</span><span class="mi">11</span><span class="p">,</span> <span class="n">shape</span><span class="p">=</span><span class="nv">rect</span><span class="p">,</span>
        <span class="n">style</span><span class="p">=</span><span class="s2">"filled,rounded"</span><span class="p">,</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#4C78A8"</span><span class="p">,</span> <span class="n">fillcolor</span><span class="p">=</span><span class="s2">"#DCE6F1"</span><span class="o">]</span><span class="p">;</span>
  <span class="k">edge</span> <span class="o">[</span><span class="n">fontname</span><span class="p">=</span><span class="s2">"Helvetica"</span><span class="p">,</span> <span class="n">fontsize</span><span class="p">=</span><span class="mi">9</span><span class="p">,</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#666666"</span><span class="p">,</span> <span class="n">arrowsize</span><span class="p">=</span><span class="mf">0.7</span><span class="o">]</span><span class="p">;</span>
  <span class="n">nodesep</span><span class="p">=</span><span class="mf">0.3</span><span class="p">;</span> <span class="n">ranksep</span><span class="p">=</span><span class="mf">0.4</span><span class="p">;</span> <span class="n">pad</span><span class="p">=</span><span class="mf">0.3</span><span class="p">;</span>

  <span class="nv">seed</span> <span class="o">[</span><span class="n">label</span><span class="p">=&lt;</span><span class="nt">&lt;b&gt;</span>hex0<span class="nt">&lt;/b&gt;</span> <span class="ni">&amp;nbsp;</span><span class="nt">&lt;font</span> <span class="na">point-size=</span><span class="s">"9"</span><span class="nt">&gt;</span>357 bytes<span class="nt">&lt;/font&gt;</span><span class="p">&gt;,</span>
        <span class="n">fillcolor</span><span class="p">=</span><span class="s2">"#F6D5D5"</span><span class="p">,</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#D62728"</span><span class="p">,</span> <span class="n">penwidth</span><span class="p">=</span><span class="mi">2</span><span class="o">]</span><span class="p">;</span>

  <span class="nv">stage0</span> <span class="o">[</span><span class="n">label</span><span class="p">=&lt;</span>hex1 → hex2 → M0 → cc_x86<span class="nt">&lt;br/&gt;&lt;font</span> <span class="na">point-size=</span><span class="s">"9"</span><span class="nt">&gt;</span>M2-Planet, kaem, M1<span class="nt">&lt;/font&gt;</span><span class="p">&gt;</span><span class="o">]</span><span class="p">;</span>
  <span class="nv">mes</span>    <span class="o">[</span><span class="n">label</span><span class="p">=&lt;</span><span class="nt">&lt;b&gt;</span>GNU Mes<span class="nt">&lt;/b&gt;</span> <span class="ni">&amp;nbsp;</span><span class="nt">&lt;font</span> <span class="na">point-size=</span><span class="s">"9"</span><span class="nt">&gt;</span>mescc<span class="nt">&lt;/font&gt;</span><span class="p">&gt;</span><span class="o">]</span><span class="p">;</span>
  <span class="nv">tccmes</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"tcc-mes"</span><span class="o">]</span><span class="p">;</span>
  <span class="nv">tcc</span>    <span class="o">[</span><span class="n">label</span><span class="p">=&lt;</span><span class="nt">&lt;b&gt;</span>tinycc<span class="nt">&lt;/b&gt;</span> <span class="ni">&amp;nbsp;</span><span class="nt">&lt;font</span> <span class="na">point-size=</span><span class="s">"9"</span><span class="nt">&gt;</span>self-hosted<span class="nt">&lt;/font&gt;</span><span class="p">&gt;</span><span class="o">]</span><span class="p">;</span>
  <span class="nv">musl</span>   <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"musl"</span><span class="o">]</span><span class="p">;</span>
  <span class="nv">bin</span>    <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"binutils"</span><span class="o">]</span><span class="p">;</span>
  <span class="nv">gcc46</span>  <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"GCC 4.6.4"</span><span class="o">]</span><span class="p">;</span>
  <span class="nv">gcc10</span>  <span class="o">[</span><span class="n">label</span><span class="p">=&lt;</span><span class="nt">&lt;b&gt;</span>GCC 10.4.0<span class="nt">&lt;/b&gt;</span> <span class="ni">&amp;nbsp;</span><span class="nt">&lt;font</span> <span class="na">point-size=</span><span class="s">"9"</span><span class="nt">&gt;</span>C++17<span class="nt">&lt;/font&gt;</span><span class="p">&gt;,</span> <span class="n">penwidth</span><span class="p">=</span><span class="mi">2</span><span class="o">]</span><span class="p">;</span>

  <span class="nv">extras</span> <span class="o">[</span><span class="n">label</span><span class="p">=&lt;</span>tar, findutils<span class="nt">&lt;br/&gt;&lt;font</span> <span class="na">point-size=</span><span class="s">"9"</span><span class="nt">&gt;</span>Linux UAPI headers<span class="nt">&lt;/font&gt;</span><span class="p">&gt;,</span>
          <span class="n">fillcolor</span><span class="p">=</span><span class="s2">"#EFEFEF"</span><span class="p">,</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#999999"</span><span class="o">]</span><span class="p">;</span>

  <span class="nv">llvm</span> <span class="o">[</span><span class="n">label</span><span class="p">=&lt;</span><span class="nt">&lt;b&gt;</span>clang 22.1.8<span class="nt">&lt;/b&gt;</span> <span class="ni">&amp;nbsp;</span>+<span class="ni">&amp;nbsp;</span> <span class="nt">&lt;b&gt;</span>lld 22.1.8<span class="nt">&lt;/b&gt;</span><span class="p">&gt;,</span>
        <span class="n">fillcolor</span><span class="p">=</span><span class="s2">"#CFE3D4"</span><span class="p">,</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#2CA02C"</span><span class="p">,</span> <span class="n">penwidth</span><span class="p">=</span><span class="mi">2</span><span class="o">]</span><span class="p">;</span>

  <span class="nv">absl</span> <span class="o">[</span><span class="n">label</span><span class="p">=&lt;</span><span class="nt">&lt;b&gt;</span>Abseil + GoogleTest<span class="nt">&lt;/b&gt;&lt;br/&gt;&lt;font</span> <span class="na">point-size=</span><span class="s">"9"</span><span class="nt">&gt;</span>236 tests pass<span class="nt">&lt;/font&gt;</span><span class="p">&gt;,</span>
        <span class="n">shape</span><span class="p">=</span><span class="nv">note</span><span class="p">,</span> <span class="n">fillcolor</span><span class="p">=</span><span class="s2">"#FFF6D5"</span><span class="p">,</span> <span class="n">color</span><span class="p">=</span><span class="s2">"#BF9000"</span><span class="o">]</span><span class="p">;</span>

  <span class="nv">seed</span> <span class="o">-&gt;</span> <span class="nv">stage0</span> <span class="o">-&gt;</span> <span class="nv">mes</span> <span class="o">-&gt;</span> <span class="nv">tccmes</span> <span class="o">-&gt;</span> <span class="nv">tcc</span> <span class="o">-&gt;</span> <span class="nv">musl</span> <span class="o">-&gt;</span> <span class="nv">bin</span> <span class="o">-&gt;</span> <span class="nv">gcc46</span> <span class="o">-&gt;</span> <span class="nv">gcc10</span><span class="p">;</span>
  <span class="nv">gcc10</span> <span class="o">-&gt;</span> <span class="nv">extras</span> <span class="o">[</span><span class="n">style</span><span class="p">=</span><span class="nv">dashed</span><span class="o">]</span><span class="p">;</span>
  <span class="nv">extras</span> <span class="o">-&gt;</span> <span class="nv">llvm</span> <span class="o">[</span><span class="n">style</span><span class="p">=</span><span class="nv">dashed</span><span class="o">]</span><span class="p">;</span>
  <span class="nv">gcc10</span> <span class="o">-&gt;</span> <span class="nv">llvm</span><span class="p">;</span>
  <span class="nv">llvm</span> <span class="o">-&gt;</span> <span class="nv">absl</span> <span class="o">[</span><span class="n">color</span><span class="p">=</span><span class="s2">"#BF9000"</span><span class="o">]</span><span class="p">;</span>

  <span class="p">{</span> <span class="n">rank</span><span class="p">=</span><span class="nv">same</span><span class="p">;</span> <span class="nv">gcc10</span><span class="p">;</span> <span class="nv">extras</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>How can I be so sure this is a hermetic toolchain?</p>

<p>The toolchain includes an <em>audit report</em> that uses Bazel’s <a href="https://bazel.build/extending/aspects">aspects</a> to inspect every action in the build graph and verify that it only executes programs built by the toolchain itself. The report is generated by running <code class="language-plaintext highlighter-rouge">bazel build //:trust-report</code> and will fail if any action executes a program outside of the Bazel output tree.<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">3</a></sup></p>

<p>The report is two lines long:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Bootstrap trust report

Every action in the checked graph runs a program built by this
repository, except for these audited seed binaries:

external/+_repo_rules+hex0-seeds/POSIX/x86/hex0-seed
/nix/store/…-bash-interactive-5.3p3/bin/bash
</code></pre></div></div>

<p>Unfortunately, since <code class="language-plaintext highlighter-rouge">genrule</code> runs a shell it takes as an absolute system path that is also listed as a seed binary. <code class="language-plaintext highlighter-rouge">sh_toolchain</code>’s <code class="language-plaintext highlighter-rouge">path</code> attribute is a string, and the shell is not a declared input of the action, so no artifact this repository built can provide it.</p>

<p>Building toolchains from bootstrap seeds was never a priority for companies like Google where they control the entire build environment.
However we seemed to have adopted the same approach as Bazel and similar build systems have become more popular in the open-source community. We should strive to make our builds more reproducible and hermetic, and this is a step in that direction.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:gcc">
      <p>Once you can reach a recent-enough GCC, you can build any C/C++ program and beyond easily. <a href="#fnref:gcc" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:llm">
      <p>Consider this the disclosure that I used an LLM to help me write the remainder of the toolchain. <a href="#fnref:llm" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:1">
      <p>We also set <code class="language-plaintext highlighter-rouge">BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1</code> to disable Bazel’s built-in C++ host toolchain detection. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[I have been fascinated and amazed by stage0 for a while now ever since I learnt about it via Guix using it to provide twenty two thousand packages source-bootstrapped from the 357-byte seed.]]></summary></entry><entry><title type="html">The Nix sandbox is a hidden input</title><link href="https://fzakaria.com/2026/07/30/the-nix-sandbox-is-a-hidden-input" rel="alternate" type="text/html" title="The Nix sandbox is a hidden input" /><published>2026-07-30T21:55:00-07:00</published><updated>2026-07-30T21:55:00-07:00</updated><id>https://fzakaria.com/2026/07/30/the-nix-sandbox-is-a-hidden-input</id><content type="html" xml:base="https://fzakaria.com/2026/07/30/the-nix-sandbox-is-a-hidden-input"><![CDATA[<p>The whole beauty of Nix was that it was incredibly pragmatic to achieve “reproducibility”, which is quite an overloaded term. The default model for Nix is the <a href="%{post_url 2025-03-08-demystifying-nix-s-intensional-model}">intensional-model</a> which is <em>input-addressed</em>: the hash of a store path is derived from the <em>recipe</em> (derivation) that produced it, and not the <em>bytes</em> that came out of it. In that framing Nix achieves <em>repeatability</em>. Nix, by default, was never bit-for-bit reproducible.<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p>For this to work, the derivation must be a <strong>complete description</strong> of the build. Anything missing from the derivation causes the <em>reproducibility</em> to break and Nix no longer to be “reproducible”.</p>

<p>In a <a href="/2026/07/30/nix-finally-has-a-source-bootstrapped-openjdk">previous post</a> I built a source-bootstrapped OpenJDK and we had to provide some additional flags:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix build .#openjdk <span class="se">\</span>
<span class="go">    --option filter-syscalls false \
    --option sandbox-paths '' \
</span><span class="c">    ...
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">--option sandbox-paths</code> mounts additional paths into the sandbox.</p>

<p>You can see the default sandbox paths on your machine with:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix config show sandbox-paths | <span class="nb">tr</span> <span class="s1">' '</span> <span class="s1">'\n'</span>
<span class="go">/bin/sh=/nix/store/zrynrzpsy2993w555ns9a734lbzfff2b-busybox-1.37.0/bin/busybox
/nix/store/cdd109fhy1axl7xb5wisv3v5pd6fawdj-qemu-aarch64-binfmt-P
/run/binfmt
</span></code></pre></div></div>

<p>Okay so what’s the point?</p>

<p>Turns out that these sandbox-paths are a <em>hidden input</em> to the derivation. The derivation does not mention it, but <strong>it can change</strong> the output meaningfully in its <em>repeatability</em> very subtley. 😬</p>

<p>Let’s explore this with a small example.</p>

<p>Here is a derivation with no dependencies. It looks for a file <code class="language-plaintext highlighter-rouge">/truth</code>. If it finds one, it believes it. If not, it falls back to arithmetic.</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># answer.nix</span>
<span class="kr">derivation</span> <span class="p">{</span>
  <span class="nv">name</span> <span class="o">=</span> <span class="s2">"answer"</span><span class="p">;</span>
  <span class="nv">system</span> <span class="o">=</span> <span class="s2">"x86_64-linux"</span><span class="p">;</span>
  <span class="nv">builder</span> <span class="o">=</span> <span class="s2">"/bin/sh"</span><span class="p">;</span>
  <span class="nv">args</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"-c"</span> <span class="s2">''</span><span class="err">
</span><span class="s2">      if [ -f /truth ]; then read -r x &lt; /truth; else x=4; fi</span><span class="err">
</span><span class="s2">      echo "2 + 2 = $x" &gt; $out</span><span class="err">
</span><span class="s2">    ''</span> <span class="p">];</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">/truth</code> is not in the store, and the Nix sandbox does not mount it, so an honest build never sees it.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix-instantiate ./answer.nix
<span class="go">/nix/store/xbik44ifqm4jqjp4z7n1031smj08mil7-answer.drv

</span><span class="gp">$</span><span class="w"> </span>nix-store <span class="nt">--realise</span> /nix/store/xbik44…-answer.drv
<span class="go">/nix/store/qba6hdgvdrry4k1z83v2zm5xy714l83m-answer

</span><span class="gp">$</span><span class="w"> </span><span class="nb">cat</span> /nix/store/qba6…-answer
<span class="go">2 + 2 = 4
</span></code></pre></div></div>

<p>Our output hash is <code class="language-plaintext highlighter-rouge">qba6hdgvdrry4k1z83v2zm5xy714l83m</code>.</p>

<p>We now can add an additional sandbox-path which will cause the repeatibility of the builder to break. We will mount a file <code class="language-plaintext highlighter-rouge">/truth</code> into the sandbox that contains a lie.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span><span class="nb">echo </span>5 <span class="o">&gt;</span> /tmp/truth
<span class="go">
</span><span class="gp">$</span><span class="w"> </span>nix-store <span class="nt">--delete</span> /nix/store/qba6…-answer      <span class="c"># throw away the honest one</span>
<span class="go">
</span><span class="gp">$</span><span class="w"> </span>nix-store <span class="nt">--realise</span> /nix/store/xbik44…-answer.drv <span class="se">\</span>
<span class="go">    --option extra-sandbox-paths "/truth=/tmp/truth"
/nix/store/qba6hdgvdrry4k1z83v2zm5xy714l83m-answer

</span><span class="gp">$</span><span class="w"> </span><span class="nb">cat</span> /nix/store/qba6…-answer
<span class="go">2 + 2 = 5
</span></code></pre></div></div>

<p>Our output hash <strong>is still</strong> <code class="language-plaintext highlighter-rouge">qba6hdgvdrry4k1z83v2zm5xy714l83m</code>. 😭</p>

<p>The derivation (<code class="language-plaintext highlighter-rouge">.drv</code>) is meant to be the complete recipe. Since the sandbox is configured outside the recipe it now causes the same derivation to be leaky and no longer hermetic. Sandboxing is often a way to enforce hermticity and here it <em>actively breaks it</em>.</p>

<p>The <code class="language-plaintext highlighter-rouge">sandbox-paths</code> are nowhere to found in the derivation.</p>

<p class="aside">This is in contrast with <code class="language-plaintext highlighter-rouge">__noChroot</code>, which is a derivation attribute.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix derivation show /nix/store/xbik44…-answer.drv
<span class="go">{
  "…-answer.drv": {
    "builder": "/bin/sh",
</span><span class="gp">    "args": ["-c", "if [ -f /truth ];</span><span class="w"> </span>…<span class="s2">"],
</span><span class="go">    "env": { "out": "…qba6…-answer", "name": "answer", … },
    "inputs": { "drvs": {}, "srcs": [] },
    …
  }
}
</span></code></pre></div></div>

<p>Two people can evaluate a byte-identical <code class="language-plaintext highlighter-rouge">.drv</code>, run <strong>different actual build steps</strong>, and Nix create the same output hash.</p>

<p>You may be thinking “Well, a derivation could have always relied on <code class="language-plaintext highlighter-rouge">/dev/random</code> or <code class="language-plaintext highlighter-rouge">date</code>, so what’s new?”. True, this is a form of <a href="https://manual.determinate.systems/advanced-topics/diff-hook.html">non-determinism</a>. The difference is those are more evident in the derivations to discover and audit for. The act of checking for the existence of a file is extremely subtle and not something that is easy to discover.</p>

<p>In fact, your derivation may appear to even be byte-reproducible on your machine with the <code class="language-plaintext highlighter-rouge">--check</code> flag, but it may not be on someone else’s machine.</p>

<p>How problematic is this?</p>

<p>The <em>default</em> value of <code class="language-plaintext highlighter-rouge">sandbox-paths</code> is not a constant baked into the source. It is a <strong>compile-time property of your particular Nix binary</strong>. Two people running <code class="language-plaintext highlighter-rouge">nix --version</code> that prints the same number can have different sandboxes before either of them touches a flag.</p>

<p>The <code class="language-plaintext highlighter-rouge">/bin/sh</code> entry is not in my <code class="language-plaintext highlighter-rouge">nix.conf</code>. It is the default, and it comes from here in the Nix source <a href="https://github.com/NixOS/nix/blob/7362ff0a5883ad122e1a98b20e9bb204e2882c75/src/libstore/globals.cc#L91">src/libstore/globals.cc</a>:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#if (defined(__linux__) || defined(__FreeBSD__)) &amp;&amp; defined(SANDBOX_SHELL)
</span>    <span class="n">sandboxPaths</span> <span class="o">=</span> <span class="p">{{</span><span class="s">"/bin/sh"</span><span class="p">,</span> <span class="p">{.</span><span class="n">source</span> <span class="o">=</span> <span class="n">SANDBOX_SHELL</span><span class="p">}}};</span>
<span class="cp">#endif
</span></code></pre></div></div>

<p>There are at least two different baselines you can build Nix with, none of them visible to any derivation:</p>

<ul>
  <li>Built with <code class="language-plaintext highlighter-rouge">sandbox-shell</code> set to a path and binary with hopefully the same semantics.</li>
  <li>Built without <code class="language-plaintext highlighter-rouge">sandbox-shell</code> such that the option is empty. No <code class="language-plaintext highlighter-rouge">/bin/sh</code> at all.</li>
</ul>

<p>Nix’s <a href="https://nix.dev/manual/nix/2.35/command-ref/conf-file.html?highlight=sandbox#conf-sandbox-paths">own documentation</a> describes this possibility.</p>

<blockquote>
  <p>Depending on how Nix was built, the default value for this option may be empty or provide <code class="language-plaintext highlighter-rouge">/bin/sh</code> as a bind-mount of <code class="language-plaintext highlighter-rouge">bash</code>.</p>
</blockquote>

<p>Although that itself is not true since <a href="https://github.com/NixOS/nixpkgs/blob/aabe13f270b979709d79f6e29cc9d8f05989d5e8/pkgs/tools/package-management/nix/modular/src/libstore/package.nix#L84">Nixpkgs builds Nix</a> with busybox not bash.</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nv">lib</span><span class="o">.</span><span class="nv">mesonOption</span> <span class="s2">"sandbox-shell"</span> <span class="s2">"</span><span class="si">${</span><span class="nv">busybox-sandbox-shell</span><span class="si">}</span><span class="s2">/bin/busybox"</span><span class="p">)</span>
</code></pre></div></div>

<p>Given that Flakes have made Nix become incredibly more decentralized, causing us to rely on <em>multiple binary caches</em>, there is no way now to guarantee that the builds are repeatable despite the fact that you can download the binary from the cache.</p>

<p>Is this a bug? 🤔</p>

<p>This is tough to say. Including the <code class="language-plaintext highlighter-rouge">sandbox-paths</code> in the derivation would make Nix completely unusable. Suppose you did fold <code class="language-plaintext highlighter-rouge">sandbox-paths</code> into the output hash by having it in the derivation. My busybox is <code class="language-plaintext highlighter-rouge">busybox-1.37.0</code> at one store path; yours is a different version at a different path. The <em>same derivation</em> would then hash to different outputs on our two machines, and binary-cache sharing across would collapse.</p>

<p>The property that lets us <em>share</em> builds is the same property that lets us <em>poison</em> them.</p>

<p>None of this is hypothetical and this is how it comes back to the OpenJDK from the top. I hit this hidden input building the OpenJDK via <a href="https://github.com/fzakaria/guixpkgs">GuixPkgs</a>, which translates Guix’s derivations into Nix and builds them with the Nix daemon.</p>

<p>Guix was forked from Nix much earlier than <a href="https://github.com/NixOS/nix/commit/a2d92bb20e82a0957067ede60e91fab256948">the commit</a> that introduced <code class="language-plaintext highlighter-rouge">--option sandbox-paths</code> and as a result <code class="language-plaintext highlighter-rouge">guix-daemon</code>’s build container has no <code class="language-plaintext highlighter-rouge">/bin</code> at all, so there is no <code class="language-plaintext highlighter-rouge">/bin/sh</code>.</p>

<p>Guix packages are built on the assumption that <code class="language-plaintext highlighter-rouge">/bin/sh</code> does not exist.
As a result when building the OpenJDK, one of the build scripts failed to have it’s shebang patched and it was expected to fail. Guix tolerates the failure and carries on. Nix, on the other hand, has <code class="language-plaintext highlighter-rouge">/bin/sh</code> and the script runs instead of failing, and the build quietly takes a different path which turns out to cause a broken output.</p>

<p class="aside">I opened <a href="https://codeberg.org/guix/guix/issues/10255">an issue</a> on Guix about this accidental behavior.</p>

<p>Making matters worse, I had already uploaded this broken output to my binary cache. Even when I discovered the <code class="language-plaintext highlighter-rouge">sandbox-paths</code> was the issue, I kept generating the same <code class="language-plaintext highlighter-rouge">.drv</code> and thus the same output hash, and was substituting the broken output.</p>

<p>Since <code class="language-plaintext highlighter-rouge">sandbox-paths</code> is a trusted user setting, I could have maliciously poisoned my own binary cache anyways by signing a broken input. This is only worse in that I did it unknowingly.</p>

<blockquote>
  <p>You can’t trust code that you did not totally create yourself.
— Ken Thompson, <a href="https://dl.acm.org/doi/pdf/10.1145/1283920.1283940">Reflections on Trusting Trust</a></p>
</blockquote>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>Nix has a <code class="language-plaintext highlighter-rouge">ca-derivations</code> feature that makes the output path a hash of the output bytes, which is a <em>different</em> model (<em>extensional-model</em>). <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[The whole beauty of Nix was that it was incredibly pragmatic to achieve “reproducibility”, which is quite an overloaded term. The default model for Nix is the intensional-model which is input-addressed: the hash of a store path is derived from the recipe (derivation) that produced it, and not the bytes that came out of it. In that framing Nix achieves repeatability. Nix, by default, was never bit-for-bit reproducible.1 Nix has a ca-derivations feature that makes the output path a hash of the output bytes, which is a different model (extensional-model). &#8617;]]></summary></entry><entry><title type="html">Nix finally has a source-bootstrapped OpenJDK</title><link href="https://fzakaria.com/2026/07/30/nix-finally-has-a-source-bootstrapped-openjdk" rel="alternate" type="text/html" title="Nix finally has a source-bootstrapped OpenJDK" /><published>2026-07-30T13:44:00-07:00</published><updated>2026-07-30T13:44:00-07:00</updated><id>https://fzakaria.com/2026/07/30/nix-finally-has-a-source-bootstrapped-openjdk</id><content type="html" xml:base="https://fzakaria.com/2026/07/30/nix-finally-has-a-source-bootstrapped-openjdk"><![CDATA[<p>One of the <a href="https://github.com/fzakaria/guixpkgs/issues/3">earliest requested issues</a> we had opened on <a href="https://github.com/fzakaria/guixpkgs">GuixPkgs</a> was to add more packages, specifically OpenJDK.</p>

<blockquote>
  <p>“Specifically I would like to see openjdk translated. openjdk is not bootstrapped from source code in nixpkgs” [<a href="https://github.com/fzakaria/guixpkgs/issues/3">issue#3</a>]</p>
</blockquote>

<p>Ever since I learned about the <a href="https://savannah.nongnu.org/projects/stage0/">stage0</a> bootstrap chain and how Guix <a href="https://guix.gnu.org/en/blog/2023/the-full-source-bootstrap-building-from-source-all-the-way-down/">announced full-source</a> bootstrap <strong>for all packages</strong> in 2023, I was in awe. They provided a package graph of more than 22,000 nodes rooted in a 357-byte program, including the JDK.<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p>GuixPkgs can now build OpenJDK 25, which means Nix
now has access to a source-bootstrap build of OpenJDK 🎉</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix build .#openjdk <span class="se">\</span>
<span class="go">    --option filter-syscalls false \
    --option sandbox-paths '' \
    --print-out-paths
/nix/store/kihk714ljmvhqh36cigc7mfimv0q0rbr-openjdk-25-guix-wrapped

</span><span class="gp">$</span><span class="w"> </span>/nix/store/kihk71…-openjdk-25-guix-wrapped/bin/java <span class="nt">--version</span>
<span class="go">openjdk 25 2025-09-16
OpenJDK Runtime Environment (build 25+-adhoc.nixbld.source)
OpenJDK 64-Bit Server VM (build 25+-adhoc.nixbld.source, mixed mode, sharing)
</span></code></pre></div></div>

<p>Does it really work? Let’s take it for a spin.</p>

<p class="aside"><code class="language-plaintext highlighter-rouge">javac</code> lives in the <code class="language-plaintext highlighter-rouge">jdk</code> output, since Guix splits the package.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix build .#openjdk.unwrapped^jdk
<span class="gp">$</span><span class="w"> </span><span class="nb">cat </span>Hello.java
<span class="gp">void main() { IO.println("Hello from a source-bootstrapped JDK");</span><span class="w"> </span><span class="o">}</span>
<span class="go">
</span><span class="gp">$</span><span class="w"> </span>./result-jdk/bin/java Hello.java
<span class="go">Hello from a source-bootstrapped JDK
</span></code></pre></div></div>

<h2 id="why-a-jdk-is-a-hard-package">Why a JDK is a hard package</h2>

<p><code class="language-plaintext highlighter-rouge">javac</code> is written in Java. <code class="language-plaintext highlighter-rouge">HotSpot</code> is C++, but the class library it needs is Java, and the compiler that compiles the class library is Java, and it runs on a JVM that needs a class library… and so on. This is a bootstrapping problem.</p>

<p>Nearly every distribution resolves this the same way in practice: download a JDK and use it to build your JDK. Debian <a href="https://wiki.debian.org/PortsDocs/BootstrappingOpenJDK">documents the pain</a>, and the Bootstrappable project has <a href="https://www.bootstrappable.org/projects/java.html">a whole page</a> on it. They are fun reads, I highly recommend them. What makes JDK special is that there is no actively maintained JDK that can be built from source without a JDK. The authors had to go back quite a few years to find one, and bring it back to life.</p>

<blockquote>
  <p>“Unfortunately, most of the software needed for the bootstrap has been abandoned.” – <a href="https://www.bootstrappable.org/projects/java.html">Bootstrappable</a></p>
</blockquote>

<p>Nixpkgs does the same. <code class="language-plaintext highlighter-rouge">openjdk-25.0.4</code> is built by <code class="language-plaintext highlighter-rouge">temurin-bin-25.0.3</code>: a 135 MB prebuilt-tarball.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix-store <span class="nt">-qR</span> <span class="si">$(</span>nix <span class="nb">eval</span> <span class="nt">--raw</span> nixpkgs#openjdk25.drvPath<span class="si">)</span> <span class="se">\</span>
<span class="go">    | grep -iE 'temurin|jdk_x64'
/nix/store/…-OpenJDK25U-jdk_x64_linux_hotspot_25.0.3_9.tar.gz.drv
/nix/store/…-temurin-bin-25.0.3.drv
</span></code></pre></div></div>

<p>Nixpkgs makes it pretty easy to audit in the <a href="https://nixos.org/manual/nixpkgs/unstable/#sec-meta-sourceProvenance">meta.sourceProvenance</a> of the package.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix <span class="nb">eval</span> <span class="nt">--json</span> nixpkgs#temurin-bin-25.meta.sourceProvenance
<span class="go">[{"isSource":false,"shortName":"binaryNativeCode"},
 {"isSource":false,"shortName":"binaryBytecode"}]
</span></code></pre></div></div>

<p>What does the source provenance of GuixPkgs’ <code class="language-plaintext highlighter-rouge">openjdk-25</code> look like?</p>

<p>It’s a little whacky but the overall build chain in Guix is the following:</p>

<ul>
  <li><strong>jikes 1.22</strong> (C++) compiles <strong>GNU Classpath 0.93</strong>, a free reimplementation of the Java class library.</li>
  <li>Classpath is enough to bring up <strong>JamVM 1.5.1</strong>, a JVM written in C. Now something can <em>run</em> Java.</li>
  <li>Now we can finally use Java tools: <strong>Ant</strong> and <strong>ecj</strong>, the Eclipse Compiler for Java.</li>
  <li>Then it doubles back and rebuilds all of that with the newer versions to get more modern JDK features.</li>
  <li>That stack finally compiles <strong>IcedTea 2.6.13</strong> (OpenJDK 7) → <strong>IcedTea 3.19.0</strong> (OpenJDK 8) → <strong>OpenJDK 9</strong>.</li>
  <li>After which it is one rung at a time: 9 builds 10, 10 builds 11, …, 24 builds 25.</li>
</ul>

<p>Nineteen complete JDK builds, from a C++ program.</p>

<h2 id="analyzing-the-closure">Analyzing the closure</h2>

<p>We can use <code class="language-plaintext highlighter-rouge">nix-store -q --graph</code> to emit the entire closure as a dot file to visualize the differences.</p>

<p class="aside">I restyled the nodes and edges: dots instead of labelled boxes, and the red to highlight the derivations that exist only because of the JDK bootstrap.</p>

<p><img src="/assets/images/openjdk-closure-hairballs.png" alt="Two force-directed renderings of complete derivation closures, stacked. The upper panel is nixpkgs' openjdk 25.0.4 with 2758 derivations, and its only red marks are two dots joined by a single short edge. The lower panel is guixpkgs' openjdk 25 with 3556 derivations, visibly wider and denser, with a constellation of 33 red nodes and their connecting edges occupying its lower left." /></p>

<p>The upper panel’s entire Java story is those two red dots and the one edge between them: <code class="language-plaintext highlighter-rouge">temurin-bin-25.0.3</code> → <code class="language-plaintext highlighter-rouge">openjdk-25.0.4</code>. The lower panel’s is the 33-node constellation: the nineteen JDKs, plus the fourteen rungs of jikes, Classpath, JamVM, Ant and ecj that had to exist before anything could compile a JDK at all.</p>

<p>Why is the Nix graph still so big if I claimed it was from a binary distribution?</p>

<p>It turns out that <strong>a JDK closure is mostly not Java.</strong></p>

<p>It is a large C++ program that wants X11, cups, fontconfig, freetype, alsa and zlib, sitting on a C toolchain that both sides bootstrap from source. That sub-graph is the same in both and it swamps everything.</p>

<p>I was curious to zoom-in and see the source-bootstrap portion of the derivation graph. Since that shared sub-graph is drowning the signal, I chose to filter it out.</p>

<p>Which derivations exist in this closure <strong>only</strong> because of how the JDK is bootstrapped? 🤔</p>

<p>That is a reachability query. We delete the Java-provenance nodes from the graph, see what is still reachable from the root, and whatever is left are the derivations that exist only because of the JDK bootstrap.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">attributable</span> <span class="o">=</span> <span class="nf">reachable</span><span class="p">(</span><span class="n">root</span><span class="p">)</span> <span class="o">-</span> <span class="nf">reachable</span><span class="p">(</span><span class="n">root</span><span class="p">,</span> <span class="n">without</span><span class="o">=</span><span class="n">java_nodes</span><span class="p">)</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>total closure</th>
      <th>sub-graph</th>
      <th>exists only for JDK</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>nixpkgs <code class="language-plaintext highlighter-rouge">openjdk-25.0.4</code></td>
      <td>2758</td>
      <td>2756</td>
      <td><strong>2</strong></td>
    </tr>
    <tr>
      <td>guixpkgs <code class="language-plaintext highlighter-rouge">openjdk-25</code></td>
      <td>3556</td>
      <td>2680</td>
      <td><strong>876</strong></td>
    </tr>
  </tbody>
</table>

<p>This lines up with our intuition. Nixpkgs only needs <strong>two</strong> derivations to bootstrap the JDK, while GuixPkgs needs <strong>876</strong>.</p>

<p><img src="/assets/images/openjdk-java-only-subgraph.svg" alt="Two pruned graphs, stacked, showing only the derivations that exist because of the JDK bootstrap. A hollow circle marks the openjdk package itself in each. The nixpkgs panel is that circle, one red dot for the prebuilt temurin binary and its tarball, on a single short line. The guixpkgs panel is a cloud of 876 grey dots with the Java packages picked out in red, clustered at two ends of a path that runs through it." /></p>

<blockquote class="alert alert-note">
  <p><strong>Note</strong>
Interestingly, the bootstrap build for JDK has <code class="language-plaintext highlighter-rouge">rustc</code> in its closure: IcedTea 8 wants GTK 2, which wants its own Mesa, which wants <code class="language-plaintext highlighter-rouge">rust-bindgen</code> 😱</p>
</blockquote>

<p>The fact that Nix can now build OpenJDK from source is thanks to the amazing work done by Guix developers.</p>

<p>What started off as a fun <em>art project</em>, started during <a href="https://tacosprint.org/">TacoSprint 2026</a>, has now found relevance to those interested in bootstraping and reproducible builds.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>Nixpkgs has made a lot of progress on this front as well, however it is still not fully bootstrapped and relies on plenty of prebuilt binaries. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[One of the earliest requested issues we had opened on GuixPkgs was to add more packages, specifically OpenJDK.]]></summary></entry><entry><title type="html">Guix by Nix</title><link href="https://fzakaria.com/2026/07/29/guix-by-nix" rel="alternate" type="text/html" title="Guix by Nix" /><published>2026-07-29T20:58:00-07:00</published><updated>2026-07-29T20:58:00-07:00</updated><id>https://fzakaria.com/2026/07/29/guix-by-nix</id><content type="html" xml:base="https://fzakaria.com/2026/07/29/guix-by-nix"><![CDATA[<p>I have been working more on <a href="https://github.com/fzakaria/guixpkgs">GuixPkgs</a> in preparation for a talk at <a href="http://nix.vegas/">nix.vegas</a> for <a href="https://defcon.org/html/defcon-34/dc-34-index.html">DEFCON34</a>. At the end of my previous <a href="/2026/06/25/guixpkgs-every-guix-package-as-a-nix-flake">GuixPkgs post</a> I left a teaser:</p>

<blockquote>
  <p>We can then build a NixOS machine where every package is the Guix equivalent 😱.</p>
</blockquote>

<p>Well, <a href="https://github.com/adeci">adeci</a><sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> took the bait and we went <em>even further</em> than that. 😈</p>

<p>Say hello to <a href="https://github.com/adeci/guix-by-nix">Guix by Nix</a>: a bootable VM where the kernel is Guix’s Linux-libre, the userland is translated Guix packages, and PID 1 is <a href="https://shepherding.services/"><strong>GNU Shepherd</strong></a>.</p>

<p>No systemd. No D-Bus. No NixOS activation. Not even a <code class="language-plaintext highlighter-rouge">nix</code> or <code class="language-plaintext highlighter-rouge">guix</code> binary in the guest. It’s Guile all the way down and Nix built all of it.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix run <span class="nt">--accept-flake-config</span> github:adeci/guix-by-nix
<span class="c">...
</span><span class="go">Starting Guix-derived initrd
GUIX_BY_NIX_INITRD_MODULES_READY modules=virtio_pci,virtio_blk,...,overlay
GUIX_BY_NIX_INITRD_ROOT_READY device=/dev/vda
Starting Guix by Nix activation
GUIX_BY_NIX_ACTIVATION_READY
Starting Guix Shepherd as PID 1
SHEPHERD_NETWORK_READY address=10.0.2.15 gateway=10.0.2.2 resolver=10.0.2.3
SHEPHERD_PID1 parent=1 child=690

Welcome to Guix by Nix (experimental)
guix-by-nix login: root
</span></code></pre></div></div>

<p>Log in with <code class="language-plaintext highlighter-rouge">root</code> / <code class="language-plaintext highlighter-rouge">guix-by-nix</code> and poke around:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">bash-5.2#</span><span class="w"> </span>ps <span class="nt">-p</span> 1
<span class="go">  PID TTY          TIME CMD
    1 ?        00:00:00 shepherd

</span><span class="gp">bash-5.2#</span><span class="w"> </span><span class="nb">uname</span> <span class="nt">-r</span>
<span class="go">6.12.62-gnu

</span><span class="gp">bash-5.2#</span><span class="w"> </span><span class="nb">readlink</span> <span class="nt">-f</span> <span class="s2">"</span><span class="si">$(</span><span class="nb">command</span> <span class="nt">-v</span> <span class="nb">ls</span><span class="si">)</span><span class="s2">"</span>
<span class="go">/nix/store/...-coreutils-9.1/bin/ls
</span></code></pre></div></div>

<p>As a reminder, even though these binaries live in <code class="language-plaintext highlighter-rouge">/nix/store</code>, they are <em>not</em> Nixpkgs packages. They were translated from Guix derivations using <a href="https://github.com/fzakaria/guix-transfer">guix-transfer</a> and built by the <code class="language-plaintext highlighter-rouge">nix-daemon</code>.</p>

<p>That <code class="language-plaintext highlighter-rouge">coreutils</code> was built from Guix’s package definition, source bootstrap and all , but it lives in <code class="language-plaintext highlighter-rouge">/nix/store</code>, because <code class="language-plaintext highlighter-rouge">nix-daemon</code> built it.</p>

<p>If you want to try out any of these packages on your own machine, you can use <a href="https://github.com/fzakaria/guixpkgs">GuixPkgs</a>.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix run <span class="nt">--accept-flake-config</span> github:fzakaria/guixpkgs#coreutils
</code></pre></div></div>

<blockquote class="alert alert-tip">
  <p><strong>Tip</strong>
Guix offers all packages built from source where Nix may offer it as a prebuilt binary. You can use GuixPkgs to get a source-built bootstrapped version of OpenJDK for example and all the <a href="https://www.bootstrappable.org/projects/java.html">whacky steps</a> to get there.</p>
</blockquote>

<h2 id="how-it-works">How it works</h2>

<p>How does this actually work? The project is a three-stage pipeline:</p>

<ol>
  <li><a href="/2026/06/05/the-guix-nix-abomination-leveraging-guix-derivations-in-nix">guix-transfer</a> is the tool that translates Guix derivation graphs into Nix derivations.</li>
  <li><a href="/2026/06/25/guixpkgs-every-guix-package-as-a-nix-flake">GuixPkgs</a> is the flake with all Guix packages, all built from the 357-byte seed, although there is a Cachix cache provided.</li>
  <li><strong>Guix by Nix</strong> assembles ~42 of those packages, a subset of the overall set, into a useable machine.</li>
</ol>

<blockquote class="alert alert-warning">
  <p><strong>Warning</strong>
AI was leveraged to write the initrd and activation scripts.
That seems to trigger people lately, so consider yourself warned.</p>
</blockquote>

<ul>
  <li>The <strong>initrd</strong> is a custom shell script, interpreted by Guix Bash, that loads eight modules, mounts the root disk and the 9p store, and <code class="language-plaintext highlighter-rouge">switch_root</code>s.</li>
  <li><strong>Activation</strong> (accounts, <code class="language-plaintext highlighter-rouge">/etc</code>, the setuid <code class="language-plaintext highlighter-rouge">sudo</code> copy) is another custom Nix-generated script run by Guix Bash.</li>
  <li><strong>PID 1</strong> is <code class="language-plaintext highlighter-rouge">shepherd</code> from Guix, with a small Scheme config that starts <code class="language-plaintext highlighter-rouge">eudev</code>, <code class="language-plaintext highlighter-rouge">dhcpcd</code>, <code class="language-plaintext highlighter-rouge">openresolv</code>, and a serial <code class="language-plaintext highlighter-rouge">getty</code>. <code class="language-plaintext highlighter-rouge">herd status</code> works like you’d expect.</li>
  <li><code class="language-plaintext highlighter-rouge">/etc/profile</code> is compiled from Guix’s own search-path specifications, so <code class="language-plaintext highlighter-rouge">GUILE_LOAD_PATH</code> and friends point where Guix intended.</li>
</ul>

<p>Every program that can be executed, every ELF file, every script interpreter, all traces back to a translated Guix derivation. The only things Nix authored are text files: the init scripts, the Shepherd config, <code class="language-plaintext highlighter-rouge">/etc</code>.</p>

<p>This is <em>Guix by Nix</em>.</p>

<h2 id="dont-trust-me-read-the-receipt">Don’t trust me. Read the receipt.</h2>

<p>“Every executable byte comes from Guix” is exactly the kind of claim that’s easy to say and easy to fudge. 🤥</p>

<p>A booted demo is great but that can’t prove it. A VM where <code class="language-plaintext highlighter-rouge">ls</code> secretly came from Nixpkgs boots <em>identically</em>.</p>

<p>The flake ships an audit-check that classifies <strong>every store path in the shipped closure</strong> by provenance, unpacks the compressed initrd<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>, and inspects every executable payload. The audit derivation fails if anything is unclassified, any ELF file or script interpreter doesn’t trace to a translated Guix output, any <code class="language-plaintext highlighter-rouge">/gnu/store</code> reference survived translation, or anything systemd-shaped appears anywhere.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>nix build <span class="nt">--accept-flake-config</span> github:adeci/guix-by-nix#status-report
<span class="gp">$</span><span class="w"> </span>jq <span class="s1">'{rejected, claims}'</span> result/status.json
<span class="go">{
  "rejected": {
    "wrapperPaths": 0,
    "nonGuixElf": 0,
    "externalScriptInterpreters": 0,
    "untranslatedGnuStoreReferences": 0,
    "unclassifiedPaths": 0,
    "systemdDbusLogindPaths": 0
  },
  "claims": {
    "translatedLinuxLibre": true,
    "wrapperFreeGuest": true,
    "allElfFromGuix": true,
    "allScriptInterpretersGuixDerived": true,
    "systemdFreeGuest": true,
    "fullyClassified": true,
    "emptyFirmwareBoundary": true
  }
}
</span></code></pre></div></div>

<p>The report includes a lot more information such as the exact Guix channel commit everything was translated from. There’s also some NixOS VM tests for good-measure. 🕵</p>

<blockquote class="alert alert-note">
  <p><strong>Note</strong>
The kernel is Guix’s <code class="language-plaintext highlighter-rouge">linux-libre-lts</code>, translated and built by <code class="language-plaintext highlighter-rouge">nix-daemon</code> like everything else. Nix wraps it in a thin adapter so NixOS’s VM tooling accepts it by augmenting with some additional metadata only; the <code class="language-plaintext highlighter-rouge">bzImage</code> is byte-for-byte Guix’s.</p>
</blockquote>

<h2 id="whats-next">What’s next</h2>

<p>One obvious next step would be a <em>normal</em> NixOS machine, where every package that exists in GuixPkgs shadows its Nixpkgs equivalent on <code class="language-plaintext highlighter-rouge">$PATH</code>. GuixPkgs offers such an overlay, but it needs quite a lot of CPU to build all of it….</p>

<p>A more realistic use case is to mix and match GuixPkgs and Nixpkgs packages in a single system. Get the best of both. I heard there were people in the Nix community who still want a non-systemd system, à la <a href="https://codeberg.org/amjoseph/sixos">sixos</a>. 🫠</p>

<p>For now, this project remains a minimal VM demo. If you make it PID 1 on real hardware, please send photos. 🙇</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>It has been extremely fun and rewarding to work with Alex on this project. We nix-pilled him at <a href="https://planetnix.org/">PlanetNix</a> 2 years ago and since then he has been pushing the boundaries of what Nix can do and is currently employed at <a href="https://www.shopify.com/">Shopify</a> working on Nix. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2">
      <p>Nix’s reference scanner can’t see inside archives and sneaky paths hide there. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[I have been working more on GuixPkgs in preparation for a talk at nix.vegas for DEFCON34. At the end of my previous GuixPkgs post I left a teaser:]]></summary></entry><entry><title type="html">The mean means nothing</title><link href="https://fzakaria.com/2026/07/27/the-mean-means-nothing" rel="alternate" type="text/html" title="The mean means nothing" /><published>2026-07-27T09:00:00-07:00</published><updated>2026-07-27T09:00:00-07:00</updated><id>https://fzakaria.com/2026/07/27/the-mean-means-nothing</id><content type="html" xml:base="https://fzakaria.com/2026/07/27/the-mean-means-nothing"><![CDATA[<p>I was recently trying to validate some performance improvements related to <code class="language-plaintext highlighter-rouge">lld</code> at <code class="language-plaintext highlighter-rouge">$DAYJOB</code> and it was a little frustrating to see the improvements in our benchmarks but not in the live-production dashboards.</p>

<p>Having come from a background working on web-services, I was used to looking at individual time-series dashboards, sometimes over a few percentiles, and I was expecing to see some noticeable change but the data seemed too noisy to make any conclusions.</p>

<p>Turns out a colleague had also faced similar issues when trying to evaluate build-speed improvements. There are lots of variables that can affect the build: cold-cache, incremental, local, remote, etc. and the build times can vary wildly depending on the state of the system and the workload. She ended up leveraging a cummulative distribution function (CDF) to visualize the data and it was a revelation to me.</p>

<p>This led me to explore a few other different ways to visualize data, in addition to the CDF, and how a single image or statistic is often not enough to tell the whole story. This post will walk through a single <em>synthetic</em> dataset and show how different visualizations can tell different stories about the same data. The goal is to convince you to <em>look</em> at your data and not just summarize it with a single number.</p>

<p>Everything below comes from one synthetic dataset with a fixed seed. The
full script can be found <a href="https://gist.github.com/fzakaria/17c72f0eddc0f10469e008e67e1385cc">in this gist</a>. It is a single file with a
<code class="language-plaintext highlighter-rouge">nix-shell</code> shebang, so you can reproduce
every figure exactly as long as you are using <a href="https://nixos.org/">nix</a>.</p>

<blockquote class="alert alert-note">
  <p><strong>Note</strong>
I leveraged AI to help generate the data and charts in this post for the story.
If that bugs you, sorry. 🤷</p>
</blockquote>

<h3 id="the-rollout-that-made-it-worse">The rollout that “made it worse”</h3>

<p>Here is the setup: we operate a typical web-service and we rolled out a new caching tier over a week, hoping to cut request latency.</p>

<p>The change is fully deployed, and the latency dashboard that plots the <strong>mean</strong> looks like this:</p>

<p><img src="/assets/images/latency-1-mean.svg" alt="Bar chart of mean latency before and after the rollout; after is 122 ms, about 9% higher than the 112 ms before" /></p>

<p>Mean latency went <em>up</em>, from 112 ms to 122 ms. ☹️</p>

<p>A SEV is cut, we revert the change and write the postmortem. Right? 🤔</p>

<h3 id="one-number-four-stories">One number, four stories</h3>

<p>It’s often good practice especially for web-services to look at various percentiles, especially the tail end of the distribution like the p95 and p99.</p>

<table>
  <thead>
    <tr>
      <th>statistic</th>
      <th>before</th>
      <th>after</th>
      <th>change</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>mean</td>
      <td>112 ms</td>
      <td>122 ms</td>
      <td><strong>+9%</strong></td>
    </tr>
    <tr>
      <td>p50 (median)</td>
      <td>99 ms</td>
      <td>54 ms</td>
      <td><strong>−46%</strong></td>
    </tr>
    <tr>
      <td>p95</td>
      <td>224 ms</td>
      <td>454 ms</td>
      <td><strong>+103%</strong></td>
    </tr>
    <tr>
      <td>p99</td>
      <td>309 ms</td>
      <td>678 ms</td>
      <td><strong>+119%</strong></td>
    </tr>
  </tbody>
</table>

<p>Now we have a problem, and the problem is that <em>everyone is right</em>. The mean says
the change is a mild regression. The median (p50), says the
change is a big-win, the typical request got <strong>nearly twice as fast</strong>. The p99
says it’s a SEV<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>, the worst requests more than doubling.</p>

<p>The mean and the median, computed from the very same numbers, point in <em>opposite
directions</em>.</p>

<p class="aside">Engineers are often taught to be data-oriented, but often it’s easy to cherry-pick the statistic that supports your argument.</p>

<h3 id="look-at-the-shape">Look at the shape</h3>

<p>The next basic thing you can do with a distribution is plot its shape.
Here are the two latency distributions, before and after, as densities:</p>

<p><img src="/assets/images/latency-2-density.svg" alt="Density plot of latency before and after; before is a single hump, after has a tall fast peak plus a second hump out in the slow region" /></p>

<p>There it is. 🤓☝️ The “before” is <strong>one tidy hump</strong>. The “after” is <strong>two humps</strong>.</p>

<p>This already explains the earlier contradiction but it’s a bit tricky to visualize correctly. The shape depends on a smoothing parameter we chose, the two fills muddy each other where they overlap, and it’s genuinely hard to read a <em>percentile</em> off it. I can see there are two populations; I can’t easily see where the median went.</p>

<h3 id="the-best-chart-youre-not-using">The best chart you’re not using</h3>

<p>The cumulative distribution function (CDF) answers one question for every percentile at once: <strong>what fraction
of requests came in at or below <em>x</em> milliseconds?</strong></p>

<p><img src="/assets/images/latency-3-cdf.svg" alt="CDF of latency before and after; the two step curves cross near 140 ms" /></p>

<p>CDFs are an extremely easy way to visualize multiple percentiles in a single chart. Depending on the curve, we can understand how the request latency is distributed across the entire population.</p>

<p>I found it incredibly useful to then plot the “before” and “after” CDFs on the same chart to see how they compare. You can then visualize the shifts at various percentiles and understand how the change affected the entire population.</p>

<p>In our story, the “after” curve has shifted left for request latencies below 140ms. That means more requests are finishing faster than before. The “after” curve is higher than the “before” curve to the right of 140ms which means more requests are finishing slower than before. The two curves cross at ~140ms which is the tipping point where the change goes from being a win to a loss.</p>

<blockquote class="alert alert-tip">
  <p><strong>Tip</strong>
Two CDFs that cross are the unmistakable signature of a change that <em>no single percentile can summarize</em>, because the sign of the effect depends on which percentile you ask.</p>
</blockquote>

<h3 id="who-won-and-by-how-much">Who won, and by how much</h3>

<p>The CDF tells us <em>that</em> the effect changes sign: faster or slower. The obvious next question is <em>by
how much</em>, at each point in the distribution. We can plot for
every percentile <em>p</em>, the after-latency minus the before-latency, known as the
<strong>shift function</strong>.</p>

<p><img src="/assets/images/latency-4-shift.svg" alt="Shift function: change in latency at each percentile, negative up to about p76 then rising steeply into the tail" /></p>

<p>Below the zero line the change is faster; above it, slower. 
We can visualize the magnitude of the change at each percentile.</p>

<h3 id="the-regression-was-there-all-along">The regression was there all along</h3>

<p>So far we have looked at two frozen snapshots, before and after. Rollouts though are often not instant. In this story, we rolled out the new caching tier over a week, ramping from 0% to 100% of traffic.</p>

<p>What did each <em>day</em> look like? Stack one distribution per day and you get a <strong>ridgeline</strong>:</p>

<p><img src="/assets/images/latency-5-ridge.svg" alt="Ridgeline plot of latency for each rollout day on a log axis; a second peak grows in as the rollout ramps from 0 to 100 percent" /></p>

<p>We can now visualize the regression emerging over time. We can see the main peak (the fast requests) sliding left as the rollout progresses, and a second peak (the slow requests) emerging on the right. The median is dropping, but the slow requests are quietly growing in number and latency.</p>

<p class="aside">The x-axis here is logarithmic. Latency is roughly lognormal, and on a linear
axis the fast peak is a tall spike next to an invisible smear; the log axis is
what lets both humps read as humps.</p>

<p>We can do something similar, squeezed into a single grid, as a <strong>heatmap</strong>. One column per day, colour for how much traffic lands at each latency:</p>

<p><img src="/assets/images/latency-5b-heatmap.svg" alt="Heatmap of latency density by rollout day; a dark main band descends while a second band appears higher up as the rollout progresses" /></p>

<p>You can sort-of make out a new population emerging faintly.</p>

<p>Any aggregate computed over the whole week would have blended these seven very different days into one muddy number and hidden the trend completely.</p>

<h3 id="the-bimodality-had-a-cause">The bimodality had a cause</h3>

<p>We’ve now thoroughly established <em>what</em> happened. The next question is <em>why</em>.
This is in fact very similar to <code class="language-plaintext highlighter-rouge">$DAYJOB</code> where I had to cut the data by binary sizes (i.e. &gt;50MiB) to observe the bimodality in the latency distribution.</p>

<p>In our story, new tier either serves a request from cache (a <strong>hit</strong>) or falls through to the backend with an extra hop (a <strong>miss</strong>). We can split the “after” requests by that property and draw a CDF for each:</p>

<p><img src="/assets/images/latency-6-filtered.svg" alt="Filtered CDF of after-rollout latency split by cache hit and miss, with the baseline as a dashed reference; two clean unimodal curves flank it" /></p>

<p>Conditioned on cache outcome, each population is unimodal again.</p>

<p>We can easily see that cache <strong>hits</strong> are faster than the old baseline as they are shifted left.</p>

<p>Cache <strong>misses</strong> pay for the extra hop and land far to the right.</p>

<h3 id="why-those-requests">Why <em>those</em> requests?</h3>

<p>“Some requests miss the cache” is a mechanism, but it isn’t yet a cause. <em>Which</em>
requests miss, and why?</p>

<p>Each request carries one more field I haven’t used yet: its <strong>response size</strong>.</p>

<p>A cache holds small, hot objects; big ones get evicted or never fit.
We can plot the latency against the response size, and colour each point by whether it was a cache hit or miss. We can also add a density to each margin to see how the two populations are distributed along each axis: a <strong>jointplot</strong>:</p>

<p><img src="/assets/images/latency-7-joint.svg" alt="Jointplot of latency versus response size for after traffic, coloured by cache outcome; cache hits cluster small-and-fast, cache misses cluster large-and-slow, with marginal densities showing the split on each axis" /></p>

<p>We can see two clean population clusters: small-and-fast (cache hits) and large-and-slow (cache misses). It’s clear that the bimodality in the latency distribution is caused by the bimodality in the response size distribution.</p>

<p>We now have something actionable: raise the cache’s max object size, or split the big responses. 🔥</p>

<h3 id="a-graph-is-worth-a-thousand-numbers">A graph is worth a thousand numbers</h3>

<p>Often a single panel or graph is too small to tell the whole story at best. At worst, it can be misleading. It is beneficial to have multiple views of the same data to understand the full story.</p>

<p>I was especially impressed with the way the CDF can convey the entire distribution in a single chart especially when comparing what might appear to be multiple populations.</p>

<blockquote>
  <p>“Do not trust any statistics you did not fake yourself.”
– Winston Churchill</p>
</blockquote>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>SEV, or “Severity”, is a popularized way to describe an incident or outage. A SEV is often numerically ranked in descending order by impact, e.g., SEV0 being the most severe. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><summary type="html"><![CDATA[I was recently trying to validate some performance improvements related to lld at $DAYJOB and it was a little frustrating to see the improvements in our benchmarks but not in the live-production dashboards.]]></summary></entry><entry><title type="html">Seriously, what is the large code-model even for?</title><link href="https://fzakaria.com/2026/07/26/seriously-what-is-the-large-code-model-even-for" rel="alternate" type="text/html" title="Seriously, what is the large code-model even for?" /><published>2026-07-26T18:59:00-07:00</published><updated>2026-07-26T18:59:00-07:00</updated><id>https://fzakaria.com/2026/07/26/seriously-what-is-the-large-code-model-even-for</id><content type="html" xml:base="https://fzakaria.com/2026/07/26/seriously-what-is-the-large-code-model-even-for"><![CDATA[<p>I have been working on making <em>massive binaries</em> possible at <code class="language-plaintext highlighter-rouge">$DAYJOB$</code>. One of the Hail Marys that you should be able to rely on is the large code-model (<code class="language-plaintext highlighter-rouge">mcmodel=large</code>) as it makes no assumptions about size and distance of relocations.</p>

<blockquote>
  <p><strong>Large code model</strong>: The large code model makes no assumptions about addresses and
sizes of sections. [<a href="https://gitlab.com/x86-psABIs/x86-64-ABI">cite</a>]</p>
</blockquote>

<p>In a <a href="/2026/03/27/does-anyone-actually-use-the-large-code-model">previous post</a>, I documented how I hit simple performance bottlenecks that made me believe that the code-model is largely theoretical in practice. In those cases, the fixes were evident and relatively small; however their omission was a hint at how no one uses the code-model because without them the performance penalty was a non-starter.</p>

<p>I hinted at some other ways I found the large code-model to still be lacking however I had not yet fully understood the pain of those failure modes.
I left a small teaser near the bottom about Thread Local Storage (<code class="language-plaintext highlighter-rouge">.tdata</code> / <code class="language-plaintext highlighter-rouge">.tbss</code>) being one of the “fun failure modes”.</p>

<p>Turns out that it is worse than I thought. The instruction sequences the compiler emits for TLS are 32-bit <em>by construction</em>, and <code class="language-plaintext highlighter-rouge">-mcmodel=large</code> has nothing to swap them for. 🤦🏻‍♂️</p>

<p>To put that very bluntly, <code class="language-plaintext highlighter-rouge">mcmodel=large</code> is incapable of building complex large binaries despite its stated goal.</p>

<h3 id="how-do-you-even-make-a-2gib-binary">How do you even make a &gt;2GiB binary?</h3>

<p>The annoying part of this whole line of research is producing a test subject. Emitting 2GiB of <em>real</em> instructions into <code class="language-plaintext highlighter-rouge">.text</code> is genuinely painful, you’d have to generate and assemble billions of instructions, and the object file is enormous. Beyond that the cardinality of the problem space grows when you consider whether the code is position-independent, do we use a procedure-linkage-table (PLT), TLS, GOT and all the various nuances each compiler brings with how they layout and order sections in their default linker scripts.</p>

<p>But relocation overflow isn’t about how many bytes are on disk, it’s about <strong>virtual-address distance</strong> between a reference and its target. So we have a few options.</p>

<p>Uninitialized globals land in <code class="language-plaintext highlighter-rouge">.bss</code>, which is a <code class="language-plaintext highlighter-rouge">NOBITS</code> section: it occupies virtual address space but <strong>zero bytes on disk</strong> (the same sparse-file trick I wrote about in <a href="/2026/02/11/creating-massively-huge-fake-files-and-binaries">massively huge fake files</a>). So a few thousand 1MiB arrays gives us gigabytes of address space essentially for free.</p>

<p>We can synthetically produce this pretty easily with the following script.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># N * 1-MiB globals, plus a function that touches each one
# so the compiler emits one relocation per array.
</span><span class="kn">import</span> <span class="n">sys</span>
<span class="n">N</span><span class="p">,</span> <span class="n">CHUNK</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]),</span> <span class="mi">1024</span> <span class="o">*</span> <span class="mi">1024</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">N</span><span class="p">):</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">char s</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s">[</span><span class="si">{</span><span class="n">CHUNK</span><span class="si">}</span><span class="s">];</span><span class="sh">"</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">long sum(void){ long a = 0;</span><span class="sh">"</span><span class="p">)</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">N</span><span class="p">):</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">  a += s</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s">[0];</span><span class="sh">"</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">  return a; }</span><span class="sh">"</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">int main(void){ return (int)sum(); }</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>python3 gen.py 4096 <span class="o">&gt;</span> huge.c
<span class="gp">$</span><span class="w"> </span>gcc <span class="nt">-c</span> huge.c <span class="nt">-o</span> huge.o
<span class="c"># 4 GiB of address space, 288 KiB on disk
</span><span class="gp">$</span><span class="w"> </span><span class="nb">du</span> <span class="nt">-h</span> huge.o
<span class="go">288K    huge.o
</span></code></pre></div></div>

<p>Link it with the (default) small code-model and it falls over exactly where you’d expect, at the 2GiB signed-32-bit boundary:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>gcc <span class="nt">-no-pie</span> huge.o <span class="nt">-o</span> huge
<span class="go">huge.c:(.text+0x780f): relocation truncated to fit: R_X86_64_PC32 against symbol `s2048' ...
</span></code></pre></div></div>

<p class="aside"><code class="language-plaintext highlighter-rouge">s2048</code> sits at <code class="language-plaintext highlighter-rouge">2048 * 1MiB</code> = precisely 2GiB.</p>

<p>Recompile the <em>same source</em> with <code class="language-plaintext highlighter-rouge">-mcmodel=large</code> and it links cleanly:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>gcc <span class="nt">-c</span> <span class="nt">-mcmodel</span><span class="o">=</span>large huge.c <span class="nt">-o</span> huge_large.o
<span class="gp">$</span><span class="w"> </span>gcc <span class="nt">-no-pie</span> <span class="nt">-mcmodel</span><span class="o">=</span>large huge_large.o <span class="nt">-o</span> huge_large
</code></pre></div></div>

<p>The large model did its job: it replaced the 32-bit <code class="language-plaintext highlighter-rouge">R_X86_64_PC32</code> references with 64-bit <code class="language-plaintext highlighter-rouge">R_X86_64_GOTOFF64</code> sequences: a <code class="language-plaintext highlighter-rouge">movabs</code> of a 64-bit offset + an <code class="language-plaintext highlighter-rouge">add</code>.</p>

<p>The <code class="language-plaintext highlighter-rouge">.bss</code> trick is great for exercising the linker, but it’s a little unsatisfying and overly synthetic. I often want to mimic relocation failures as they traverse a large <code class="language-plaintext highlighter-rouge">.text</code> segment.
We can leverage the assembler’s <code class="language-plaintext highlighter-rouge">.fill</code> directive to repeat instructions. We can generate a tiny source of N functions spaced 1MiB apart with a <code class="language-plaintext highlighter-rouge">NOP</code> sea between them, plus one dispatcher that <code class="language-plaintext highlighter-rouge">call</code>s every function. This requires each <code class="language-plaintext highlighter-rouge">call</code> to have a relocation and the calls to functions past the 2GiB mark overflow. 🔥</p>

<details>
  <summary>Generator: <code>gen_realtext.py</code></summary>

  <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># N tiny functions spaced 1 MiB apart, plus a dispatcher that calls each one.
# .fill is a memset, so the assembler emits the bytes without codegen.
</span><span class="kn">import</span> <span class="n">sys</span>

<span class="n">N</span><span class="p">,</span> <span class="n">CHUNK</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]),</span> <span class="mi">1024</span> <span class="o">*</span> <span class="mi">1024</span>   <span class="c1"># e.g. 4096 -&gt; ~4 GiB of .text
</span>
<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">.text</span><span class="sh">"</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">.globl main</span><span class="sh">"</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">main:</span><span class="sh">"</span><span class="p">)</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">N</span><span class="p">):</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">  call f</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>               <span class="c1"># one relocation per function
</span><span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">  ret</span><span class="sh">"</span><span class="p">)</span>

<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">N</span><span class="p">):</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">.globl f</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">f</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s">:</span><span class="sh">"</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">  ret</span><span class="sh">"</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">  .fill </span><span class="si">{</span><span class="n">CHUNK</span><span class="si">}</span><span class="s">, 1, 0x90</span><span class="sh">"</span><span class="p">)</span>   <span class="c1"># 1 MiB of real NOP bytes -&gt; real .text
</span></code></pre></div>  </div>

</details>

<h3 id="a-quick-primer-on-tls-access-models">A quick primer on TLS access models</h3>

<p>Now let’s do the exact same thing, but using <code class="language-plaintext highlighter-rouge">__thread</code>.</p>

<p>When you access a thread-local variable, the compiler doesn’t just load an address, it emits one of four <em>access models</em>, from fastest/least-flexible to slowest/most-flexible:</p>

<dl>
  <dt><strong>Local Exec (LE)</strong></dt>
  <dd>The variable lives in the main executable’s own TLS block. The offset from the thread pointer (<code class="language-plaintext highlighter-rouge">%fs</code>) is a link-time constant, baked directly into the instruction as a 32-bit immediate; uses <code class="language-plaintext highlighter-rouge">R_X86_64_TPOFF32</code>.</dd>
  <dt><strong>Initial Exec (IE)</strong></dt>
  <dd>The variable is in a module loaded at startup. The 64-bit thread-pointer offset is stored in a <strong>GOT slot</strong>, and the code loads it with a RIP-relative reference; uses <code class="language-plaintext highlighter-rouge">R_X86_64_GOTTPOFF</code> to reach the slot &amp; <code class="language-plaintext highlighter-rouge">R_X86_64_TPOFF64</code> to fill the slot.</dd>
  <dt><strong>General Dynamic (GD)</strong> / <strong>Local Dynamic (LD)</strong></dt>
  <dd>The fully general case for <code class="language-plaintext highlighter-rouge">dlopen</code>‘d modules, a call into <code class="language-plaintext highlighter-rouge">__tls_get_addr</code>; uses <code class="language-plaintext highlighter-rouge">R_X86_64_TLSGD</code> &amp; <code class="language-plaintext highlighter-rouge">R_X86_64_TLSLD</code>.</dd>
</dl>

<p>Notice the pattern: the value that reaches the thread pointer can be 64-bit (i.e. it lives in a GOT slot), but <strong>every instruction that participates in a TLS access uses a 32-bit field</strong>. <code class="language-plaintext highlighter-rouge">TPOFF32</code>, <code class="language-plaintext highlighter-rouge">GOTTPOFF</code>, <code class="language-plaintext highlighter-rouge">TLSGD</code>, <code class="language-plaintext highlighter-rouge">TLSLD</code> are all 32-bit.</p>

<details>
  <summary>Generator: <code>gen_tls.py</code></summary>

  <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Same as gen.py, but every array is thread-local (__thread) so it lands in
# .tbss and each access emits a TLS relocation instead of a plain data one.
</span><span class="kn">import</span> <span class="n">sys</span>

<span class="n">N</span><span class="p">,</span> <span class="n">CHUNK</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]),</span> <span class="mi">1024</span> <span class="o">*</span> <span class="mi">1024</span>   <span class="c1"># e.g. 4096 -&gt; ~4 GiB of .tbss
</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">N</span><span class="p">):</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">__thread char t</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s">[</span><span class="si">{</span><span class="n">CHUNK</span><span class="si">}</span><span class="s">];</span><span class="sh">"</span><span class="p">)</span>

<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">long sum(void){ long a = 0;</span><span class="sh">"</span><span class="p">)</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">N</span><span class="p">):</span>
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">  a += t</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="s">[0];</span><span class="sh">"</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">  return a; }</span><span class="sh">"</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">int main(void){ return (int)sum(); }</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div>  </div>

</details>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>python3 gen_tls.py 4096 <span class="o">&gt;</span> tls_huge.c   <span class="c"># like gen.py but each array is `__thread`</span>
<span class="gp">$</span><span class="w"> </span>gcc <span class="nt">-c</span> <span class="nt">-mcmodel</span><span class="o">=</span>large tls_huge.c <span class="nt">-o</span> tls_huge.o
<span class="gp">$</span><span class="w"> </span><span class="nb">du</span> <span class="nt">-h</span> tls_huge.o
<span class="go">288K    tls_huge.o
</span></code></pre></div></div>

<p>4GiB of <code class="language-plaintext highlighter-rouge">.tbss</code>, still tiny on disk. Now look at the relocations the <strong>large</strong> code-model generated:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>readelf <span class="nt">-r</span> tls_huge.o | <span class="nb">awk</span> <span class="s1">'{print $3}'</span> | <span class="nb">grep </span>R_X86 | <span class="nb">sort</span> | <span class="nb">uniq</span> <span class="nt">-c</span>
<span class="go">      1 R_X86_64_GOTOFF64
      2 R_X86_64_GOTPC64
      2 R_X86_64_PC32
   4096 R_X86_64_TPOFF32
</span></code></pre></div></div>

<p>Every single TLS access is <code class="language-plaintext highlighter-rouge">R_X86_64_TPOFF32</code> which is <strong>32-bit</strong>, even under <code class="language-plaintext highlighter-rouge">-mcmodel=large</code>. 🫣</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>gcc <span class="nt">-no-pie</span> <span class="nt">-mcmodel</span><span class="o">=</span>large tls_huge.o <span class="nt">-o</span> tls_huge
<span class="go">tls_huge.c:(.text+0x25): relocation truncated to fit: R_X86_64_TPOFF32 against symbol `t0' defined in .tbss ...
tls_huge.c:(.text+0x36): relocation truncated to fit: R_X86_64_TPOFF32 against symbol `t1' ...
</span></code></pre></div></div>

<p>The <strong>exact same large code-model</strong> that just happily linked 4GiB of ordinary <code class="language-plaintext highlighter-rouge">.bss</code> <strong>fails</strong> on 4GiB of <code class="language-plaintext highlighter-rouge">.tbss</code>.</p>

<p>This isn’t a GNU quirk either. LLVM (<code class="language-plaintext highlighter-rouge">clang</code> &amp; <code class="language-plaintext highlighter-rouge">lld</code>) does exactly the same thing, albeit with <code class="language-plaintext highlighter-rouge">lld</code>’s diagnostic being a bit friendly by printing the actual offset and the window it has to fit in:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>clang <span class="nt">-c</span> <span class="nt">-mcmodel</span><span class="o">=</span>large tls_huge.c <span class="nt">-o</span> tls_huge.o
<span class="gp">$</span><span class="w"> </span>clang <span class="nt">-no-pie</span> <span class="nt">-fuse-ld</span><span class="o">=</span>lld <span class="nt">-mcmodel</span><span class="o">=</span>large tls_huge.o <span class="nt">-o</span> tls_huge
<span class="go">ld.lld: error: tls_huge.o:(function sum: .ltext+0x17): relocation R_X86_64_TPOFF32
</span><span class="gp">  out of range: -4294967296 is not in [-2147483648, 2147483647];</span><span class="w"> </span>references <span class="s1">'t0'</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">-4294967296</code> is exactly <code class="language-plaintext highlighter-rouge">-4GiB</code>, <code class="language-plaintext highlighter-rouge">t0</code> sitting at the far bottom of the TLS block, and <code class="language-plaintext highlighter-rouge">[-2147483648, 2147483647]</code> is the signed 32-bit window <code class="language-plaintext highlighter-rouge">TPOFF32</code> has to live in.</p>

<p class="aside"><code class="language-plaintext highlighter-rouge">lld</code> even parks large-model code in a <code class="language-plaintext highlighter-rouge">.ltext</code> section.</p>

<h3 id="why-the-large-model-is-powerless-here">Why the large model is powerless here</h3>

<p>I was a little confused at first: <code class="language-plaintext highlighter-rouge">R_X86_64_TPOFF64</code> <strong>exists</strong>. Why doesn’t the compiler use it, especially with <code class="language-plaintext highlighter-rouge">mcmodel=large</code> ?</p>

<p>A relocation type <strong>is glued to the specific field it patches</strong> and is determined by the code-sequence emitted by the compiler. Here’s the <strong>local-exec</strong> sequence <code class="language-plaintext highlighter-rouge">-mcmodel=large</code> emits:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">   4: 64 48 8b 04 25 00 00   mov    %fs:0x0,%rax
</span><span class="gp">   d: 48 05 00 00 00 00      add    $</span>0x0,%rax
<span class="go">                             R_X86_64_TPOFF32  x
  13: 8b 00                  mov    (%rax),%eax
</span></code></pre></div></div>

<p>The offset is applied with <code class="language-plaintext highlighter-rouge">add $imm32, %rax</code>. The x86-64 encoding for that instruction has <strong>only a 32-bit immediate field</strong>. There is no 64-bit form of it. The only relocation that can physically patch that field is a 32-bit one: <code class="language-plaintext highlighter-rouge">TPOFF32</code>.</p>

<p>So where <em>is</em> <code class="language-plaintext highlighter-rouge">TPOFF64</code> used? It’s not in the code (<code class="language-plaintext highlighter-rouge">.text</code>) at all. It lives as a dynamic relocation on the <strong>GOT slot</strong> used by <strong>initial-exec</strong>. The code side only holds the 32-bit <code class="language-plaintext highlighter-rouge">GOTTPOFF</code> that <em>points</em> at the slot:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">   4: 48 8b 0d 00 00 00 00   mov    0x0(%rip),%rcx
                             R_X86_64_GOTTPOFF  y
   b: 64 48 8b 04 25 00 00   mov    %fs:0x0,%rax
  14: 48 01 c8               add    %rcx,%rax
  17: 8b 00                  mov    (%rax),%eax
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">TPOFF64</code> patches the 8-byte (64bit) <em>data word</em> in the GOT. The instruction that <em>reaches</em> that word (<code class="language-plaintext highlighter-rouge">GOTTPOFF</code>) is a 32-bit PC-relative relocation. So even initial-exec has a 32-bit link in the chain: the GOT slot must sit within 2GiB of the code. 😭</p>

<h3 id="it-fails-for-exactly-the-mode-its-meant-for">It fails for exactly the mode it’s meant for</h3>

<p><strong>local-exec</strong> is the mode you <em>want</em> for a big statically-linked executable’s own thread-locals. It’s the fast path: the thread-pointer offset is a link-time constant, so the access is a direct <code class="language-plaintext highlighter-rouge">add $imm32</code> with no memory load, no GOT, no indirection. Unfortunately, <code class="language-plaintext highlighter-rouge">TPOFF32</code> is limited to 32bits and overflows.</p>

<p class="aside">I would argue it’s also the mode most likely to <em>need</em> a large binary, since it’s what a giant static executable uses for its own TLS.</p>

<p>What if we fall back to a slower mode? Unfortunately, each one of them has a 32-bit field too:</p>

<ul>
  <li><strong>initial-exec</strong> <code class="language-plaintext highlighter-rouge">GOTTPOFF</code> is 32-bit RIP-relative reach to the GOT slot.</li>
  <li><strong>general-dynamic</strong> <code class="language-plaintext highlighter-rouge">TLSGD</code> is 32-bit RIP-relative.</li>
</ul>

<p>The use of <code class="language-plaintext highlighter-rouge">GOTTPOFF</code> seems especially confusing. The large code-model already reaches the GOT at 64 bits everywhere else: GOTPC64 for the base, GOT64 for the slot, so the omission seems surprising. The 64-bit machinery is right there. TLS just doesn’t use it. 🥲</p>

<h3 id="this-is-a-hole-in-the-abi-not-a-compiler-bug">This is a hole in the ABI, not a compiler bug</h3>

<p>After having dug into it, it’s not simply a bug in GCC or LLVM. Look again at what <code class="language-plaintext highlighter-rouge">clang</code> emits for a single <strong>local-exec</strong> access under <code class="language-plaintext highlighter-rouge">-mcmodel=large</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">   4: movq %fs:0x0, %rax
</span><span class="gp">   d: addq $</span>0x0, %rax           <span class="c"># R_X86_64_TPOFF32 x</span>
<span class="go">  13: movl (%rax), %eax
</span></code></pre></div></div>

<p>It’s a plain <code class="language-plaintext highlighter-rouge">addq $imm32, %rax</code>. There is nothing stopping clang from emitting a 64-bit form instead:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>movabs $x@tpoff, %rdx      # a TPOFF64 in a movabs immediate
addq   %fs:0, %rdx
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">TPOFF64</code> could patch that immediate, exactly the way <code class="language-plaintext highlighter-rouge">GOTOFF64</code> already patches a <code class="language-plaintext highlighter-rouge">movabs</code> immediate for ordinary data.</p>

<p>Oddly, <strong>the relocation type is not the missing piece.</strong> What’s missing is a <em>code sequence</em> in the <a href="https://gitlab.com/x86-psABIs/x86-64-ABI">x86-64 psABI</a> that uses a 64-bit TLS offset as an instruction immediate. The ABI simply never defined a large code-model TLS access model, so <code class="language-plaintext highlighter-rouge">TPOFF64</code>/<code class="language-plaintext highlighter-rouge">DTPOFF64</code> only ever appear in GOT slots, never in code, and no toolchain can emit what the spec doesn’t describe.</p>

<p>The large code-model “exists”, but it doesn’t actually deliver arbitrarily-large binaries. For thread-local storage it can’t, because the specification never defined how to.</p>

<p>This is one more reason I’m working toward making massive binaries possible. If you want to follow along, the discussion is over in the <a href="https://groups.google.com/g/x86-64-abi/c/hz28LNnlBEc/m/J211uZASAgAJ">x86-64-abi</a> google-group where I’ve posted an RFC, and we have started an LLVM <a href="https://discourse.llvm.org/t/rfc-forming-a-massive-binaries-working-group-in-lld/91031">Massive Binaries working group</a> and hold monthly meetings.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I have been working on making massive binaries possible at $DAYJOB$. One of the Hail Marys that you should be able to rely on is the large code-model (mcmodel=large) as it makes no assumptions about size and distance of relocations.]]></summary></entry><entry><title type="html">Small site changes and new design</title><link href="https://fzakaria.com/2026/07/26/small-site-changes-and-new-design" rel="alternate" type="text/html" title="Small site changes and new design" /><published>2026-07-26T13:59:00-07:00</published><updated>2026-07-26T13:59:00-07:00</updated><id>https://fzakaria.com/2026/07/26/small-site-changes-and-new-design</id><content type="html" xml:base="https://fzakaria.com/2026/07/26/small-site-changes-and-new-design"><![CDATA[<p>I have been wanting to refresh my site for a while now. 
I have even posted a few times on <a href="https://www.linkedin.com/">LinkedIn</a> and <a href="http://x.com/">X</a> offering to pay if somoene was interested in taking a stab at it.</p>

<p>I am sure this will put people off from reading my blog just stating this, but I decided to take a stab at some design changes with AI and some minor site improvements that I have always wanted to do.</p>

<p>If you think the site looks much worse, <a href="mailto:farid.m.zakaria@gmail.com">please let me know</a>.</p>

<p>Some small improvements and quality-of-life enhacements:</p>

<ul>
  <li>A small <code class="language-plaintext highlighter-rouge">/nix/store</code> entry in the footer to showcase the site is built with Nix.</li>
  <li>A <a href="https://github.com/fzakaria/fzakaria.com/blob/9d5dc190358f42466d99ce990cda77448f8713d8/_plugins/images.rb">Jekyll plugin</a> that automatically creates source-sets for the images I use in my blog posts.</li>
  <li>A <a href="https://github.com/fzakaria/fzakaria.com/blob/9d5dc190358f42466d99ce990cda77448f8713d8/_plugins/asset_version.rb">Jekyll plugin</a> to create a short content hash for the stylesheets to avoid the browsers caching stale CSS.</li>
  <li>A <a href="https://github.com/fzakaria/fzakaria.com/blob/779849a9a37e2a8c381acc751eacac5dd4b1aa3f/_plugins/signal.rb">Jekyll plugin</a> that generates a little curved line for the masthead that is the <code class="language-plaintext highlighter-rouge">sha256</code> of the page.</li>
  <li>Redesigned the layout to allow side-bar content.</li>
</ul>

<p class="aside">Look at me! I’m sidebar content I will undoubtedly abuse.</p>

<p>I continued to try and give it my personal <em>flair</em> to avoid it looking like <em>AI-slop</em> but 🤷</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I have been wanting to refresh my site for a while now. I have even posted a few times on LinkedIn and X offering to pay if somoene was interested in taking a stab at it.]]></summary></entry><entry><title type="html">Linux kernel will support $ORIGIN, sort of</title><link href="https://fzakaria.com/2026/07/20/linux-kernel-will-support-origin-sort-of" rel="alternate" type="text/html" title="Linux kernel will support $ORIGIN, sort of" /><published>2026-07-20T19:00:00-07:00</published><updated>2026-07-20T19:00:00-07:00</updated><id>https://fzakaria.com/2026/07/20/linux-kernel-will-support-origin-sort-of</id><content type="html" xml:base="https://fzakaria.com/2026/07/20/linux-kernel-will-support-origin-sort-of"><![CDATA[<p>For some reason, during <a href="https://tacosprint.org">TacoSprint 2026</a> I decided to see if we could tackle <a href="/2026/06/21/nix-needs-relocatable-binaries">relocatable binaries</a> in Nix.</p>

<p>I enjoy these lofty goals to push Nix and the surrounding ecosystem forward. I am <em>bold if not stupid</em>.</p>

<p>I left the last earlier post with one potential idea of how to get there:</p>

<blockquote>
  <p>We could patch the Linux kernel so that $ORIGIN is supported in PT_INTERP and the shebang.</p>
</blockquote>

<p>I waded through the complexity of sending patches over email (turns out I actually enjoy this workflow!), and sent a proposal to the Linux kernel mailing list.</p>

<p>My first attempt <a href="https://lore.kernel.org/all/20260622043934.179879-1-farid.m.zakaria@gmail.com/">here</a> proposed simply adding direct support for <code class="language-plaintext highlighter-rouge">$ORIGIN</code> in the Virtual File System (VFS) subsystem.</p>

<p>I waited nervously. I was expecting the result from what I had come to read about online; someone non-politely telling me to <em>F$#CK OFF</em> because there is something I missed, misunderstood or did not consider. 🤬</p>

<p>The result was completely different. 😲</p>

<p><a href="https://brauner.io/">Christian Brauner</a>, the maintainer for VFS responded to me in good faith, asking for the rationale for the change and eventually proposing some ways in which such a support could make it into the subsystem.</p>

<blockquote class="alert alert-note">
  <p><strong>Note</strong>
It definitely helped having someone like <a href="https://github.com/ericson2314">John Ericson</a> <a href="https://lore.kernel.org/all/24420045-a6eb-4999-ab19-1e344eaba8a4@app.fastmail.com/">chime in</a> and advocate why having a non-fixed interpreter (<code class="language-plaintext highlighter-rouge">PT_INTERP</code>) is useful to Nix and other use-cases (i.e. Buck &amp; Bazel).</p>
</blockquote>

<p>He offered that potentially we could leverage <a href="https://ebpf.io/">eBPF</a> as a programmable way to select an interpreter through <a href="https://docs.kernel.org/admin-guide/binfmt-misc.html">binfmt_misc</a>.</p>

<p>Whoa! 🤯</p>

<p>I wanted to merely allow <code class="language-plaintext highlighter-rouge">$ORIGIN</code> but a programmable selection could let us do anything!</p>

<p>The idea must have really intrigued him because soon-after, <em>on his vacation</em>, Christian offered the first draft of such a solution. We went back and forth a little over the mailing list and the end result is a <a href="https://lore.kernel.org/linux-fsdevel/20260716-wacholderbeere-zahlt-beraten-e872c3a4f59b@brauner/T/#ma8bbf0640f3154d76e1fd4607c61507b72609c6a">patch series</a> that will make its way into <code class="language-plaintext highlighter-rouge">-next</code> branch in the near future.</p>

<p>If you don’t know what eBPF is or <code class="language-plaintext highlighter-rouge">binfmt_misc</code>, WTF did we just collaborate on?</p>

<p>Let’s take a look!</p>

<p>I won’t do eBPF justice, and there are plenty of articles online about it as it’s quite <em>in-vogue</em> at the moment.</p>

<p><strong>tl;dr;</strong> You can write programs in a C subset that gets compiled to an instruction set whose virtual machine is running <strong>within the kernel</strong>. Shouldn’t the kernel be super fast? Yes, the programs are jitted to their native CPU architecture and the programs have a fixed-time slice. Isn’t this some crazy vulnerability for the kernel? Before any code is loaded it is “verified” to be safe. Checkout <a href="https://ebpf.io/what-is-ebpf/">this guide</a> for more info.</p>

<p>We can now support <code class="language-plaintext highlighter-rouge">$ORIGIN</code> with a relatively simple eBPF program:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">SEC</span><span class="p">(</span><span class="s">"struct_ops.s/match"</span><span class="p">)</span>
<span class="n">bool</span> <span class="nf">BPF_PROG</span><span class="p">(</span><span class="n">nix_match</span><span class="p">,</span> <span class="k">struct</span> <span class="n">linux_binprm</span> <span class="o">*</span><span class="n">bprm</span><span class="p">)</span>
<span class="p">{</span>
  <span class="k">return</span> <span class="o">!</span><span class="n">bpf_strncmp</span><span class="p">(</span><span class="n">bprm</span><span class="o">-&gt;</span><span class="n">buf</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="s">"</span><span class="se">\x7f</span><span class="s">"</span> <span class="s">"ELF"</span><span class="p">);</span>
<span class="p">}</span>

<span class="n">SEC</span><span class="p">(</span><span class="s">"struct_ops.s/load"</span><span class="p">)</span>
<span class="kt">int</span> <span class="nf">BPF_PROG</span><span class="p">(</span><span class="n">nix_load</span><span class="p">,</span> <span class="k">struct</span> <span class="n">linux_binprm</span> <span class="o">*</span><span class="n">bprm</span><span class="p">)</span>
<span class="p">{</span>
  <span class="kt">char</span> <span class="n">path</span><span class="p">[</span><span class="mi">256</span><span class="p">];</span>
  <span class="kt">long</span> <span class="n">n</span><span class="p">;</span>

  <span class="n">n</span> <span class="o">=</span> <span class="n">bpf_path_d_path</span><span class="p">(</span><span class="o">&amp;</span><span class="n">bprm</span><span class="o">-&gt;</span><span class="n">file</span><span class="o">-&gt;</span><span class="n">f_path</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">path</span><span class="p">));</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">n</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">n</span><span class="p">;</span>

  <span class="cm">/* derive the loader location from the binary's path */</span>

  <span class="k">return</span> <span class="n">bpf_binprm_set_interp</span><span class="p">(</span><span class="n">bprm</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">path</span><span class="p">));</span>
<span class="p">}</span>

<span class="n">SEC</span><span class="p">(</span><span class="s">".struct_ops.link"</span><span class="p">)</span>
<span class="k">struct</span> <span class="n">binfmt_misc_ops</span> <span class="n">nix</span> <span class="o">=</span> <span class="p">{</span>
  <span class="p">.</span><span class="n">match</span> <span class="o">=</span> <span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="n">nix_match</span><span class="p">,</span>
  <span class="p">.</span><span class="n">load</span> <span class="o">=</span> <span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="n">nix_load</span><span class="p">,</span>
  <span class="p">.</span><span class="n">name</span> <span class="o">=</span> <span class="s">"nix"</span><span class="p">,</span>
<span class="p">};</span>
</code></pre></div></div>

<p>Once the above program is loaded and registered into the kernel, we then ask the <code class="language-plaintext highlighter-rouge">binfmt_misc</code> subsystem to trigger it. Checkout <a href="https://lore.kernel.org/linux-fsdevel/20260711-binfmt-misc-bpf-v2-v2-5-d6591ceaf207@gmail.com/">this thread</a> if you want to see the complete example.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">&gt;</span><span class="w"> </span>bpftool struct_ops register nix_origin.bpf.o /sys/fs/bpf
<span class="gp">&gt;</span><span class="w"> </span><span class="nb">echo</span> <span class="s1">':origin:B::::nix:'</span> <span class="o">&gt;</span> /proc/sys/fs/binfmt_misc/register
</code></pre></div></div>

<p>What does that mean?</p>

<p>It means that every binary now triggers the <code class="language-plaintext highlighter-rouge">nix_match</code> function above, in this case any <code class="language-plaintext highlighter-rouge">ELF</code> file, but it could be executables with a new segment like <code class="language-plaintext highlighter-rouge">PT_INTERP_NIX</code>, and the kernel will ask <code class="language-plaintext highlighter-rouge">nix_load</code> to determine the interpreter to use dynamically.</p>

<p>Our special BPF program has support for <code class="language-plaintext highlighter-rouge">$ORIGIN</code> 💥</p>

<p>What else could you do?</p>

<p>Well we can now even completely replace the traditional QEMU <code class="language-plaintext highlighter-rouge">binfmt_misc</code> <a href="https://github.com/qemu/qemu/blob/master/scripts/qemu-binfmt-conf.sh">registration script</a> with a BPF program now like <a href="https://gist.github.com/fzakaria/bef27d2e21b0e36ffccda1cbf417b636">this one</a>.</p>

<p>What else can we do?</p>

<p>Since we can now programmatically select our interpreter <strong>based on anything</strong> in the file, we can do quite a lot. I’m keen to hear your suggestions and ideas 💡.</p>

<p>Some of the smaller items are that we can even support <code class="language-plaintext highlighter-rouge">$ORIGIN</code> in the shebangs (<code class="language-plaintext highlighter-rouge">#!$ORIGIN/bin/ld.so</code>) very easily as <a href="https://gist.github.com/fzakaria/2e1e1c44fa488a951674f8761c672366">seen here</a>: we simply look at the first 256 bytes of the file and look for <code class="language-plaintext highlighter-rouge">$ORIGIN</code> to trigger.</p>

<p>One downside or <em>side-effect</em> of the traditional <code class="language-plaintext highlighter-rouge">binfmt_misc</code> hand-off was that the way in which the desired final binary was invoked was <em>non-transparent</em>.</p>

<p>The registered interpreter <strong>becomes</strong> the process. It owns the entire process identity, and the binary you actually asked to run gets demoted to an argument. For <code class="language-plaintext highlighter-rouge">wine</code> or <code class="language-plaintext highlighter-rouge">qemu</code> that’s acceptable as they are emulators  but for a per-binary BPF loader that might pick a traditional <code class="language-plaintext highlighter-rouge">ld.so</code> it does not make much sense.</p>

<p>This leaks in a few painful ways but the simplest are :</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">argv[0]</code> and <code class="language-plaintext highlighter-rouge">/proc/&lt;pid&gt;/cmdline</code> show the <em>interpreter</em> invocation, not what you executed.</li>
  <li><code class="language-plaintext highlighter-rouge">/proc/self/exe</code> names the interpreter. Relocatable programs commonly locate <em>themselves</em> through <code class="language-plaintext highlighter-rouge">/proc/self/exe</code>, and instead they find the dynamic linker. 😩</li>
</ul>

<p>Christian sent a large patch series for this as well. His latest <a href="https://lore.kernel.org/linux-fsdevel/20260720-work-bpf-binfmt_misc-ptinterp-v1-0-ddb76c9a508e@kernel.org/T/#m5c7c7cbf4e19d2f045a69f5a1284220d6c35d88c">patch series</a> adds <strong>two</strong> new dispatch modes that close the gap from opposite ends and covers a few other <em>gotchas</em> that these modes can fix.</p>

<p>The <strong>loader substitition</strong> <code class="language-plaintext highlighter-rouge">L</code> is the one I’m most excited about for Nix.</p>

<p>With the <code class="language-plaintext highlighter-rouge">L</code> flag, the kernel executes the matched binary <strong>natively</strong> as the main image, and merely substitutes the registered interpreter for the loader named in the binary’s <code class="language-plaintext highlighter-rouge">PT_INTERP</code>. <code class="language-plaintext highlighter-rouge">binfmt_misc</code> stops being a hand-off and becomes a plain <code class="language-plaintext highlighter-rouge">PT_INTERP</code> override. There’s no contract and no identity to reconstruct, so a <strong>stock dynamic loader works unchanged</strong>.</p>

<p>Where does this leaves us?</p>

<p>I’ll be tracking the Linux kernel releases and, once this lands in <code class="language-plaintext highlighter-rouge">-next</code> and ships in a tagged release, I plan to upstream a <strong>NixOS module</strong> that registers the <code class="language-plaintext highlighter-rouge">$ORIGIN</code> support at boot. 🎉</p>

<p>The plan is to gate it on a new <code class="language-plaintext highlighter-rouge">PT_INTERP_NIX</code> segment rather than matching every <code class="language-plaintext highlighter-rouge">ELF</code> file. That keeps things <strong>backwards compatible</strong>: the BPF handler only kicks in for binaries that explicitly opt-in by carrying the new segment. This means Nix produced binaries continue to work without the BFP handler but those that have it may elevate themselves to <em>relocatable status</em>.</p>

<blockquote>
  <p>A ship in harbor is safe, but that is not what ships are built for.
 — John A. Shedd</p>
</blockquote>]]></content><author><name></name></author><summary type="html"><![CDATA[For some reason, during TacoSprint 2026 I decided to see if we could tackle relocatable binaries in Nix.]]></summary></entry><entry><title type="html">How to piss off your Nix friends</title><link href="https://fzakaria.com/2026/07/18/how-to-piss-off-your-nix-friends" rel="alternate" type="text/html" title="How to piss off your Nix friends" /><published>2026-07-18T15:45:00-07:00</published><updated>2026-07-18T15:45:00-07:00</updated><id>https://fzakaria.com/2026/07/18/how-to-piss-off-your-nix-friends</id><content type="html" xml:base="https://fzakaria.com/2026/07/18/how-to-piss-off-your-nix-friends"><![CDATA[<blockquote class="alert alert-warning">
  <p><strong>Warning</strong>
If you are <strong>pissed off</strong> reading this blog post, I guess, mission accomplished ? Try not to take life too seriously. Unfortunately, there’s a lot worse things in life than my opinions on Nix.</p>
</blockquote>

<p>Seems like it’s all too easy to get people in Nix flustered, angry and out with their pitchforks. All it takes is <a href="https://github.com/NixOS/nixpkgs/pull/534657">someone proposing a markdown file</a> for the community to lose its mind.</p>

<p>Having been a member, exposed to and part of the Nix/NixOS community for many years, I thought I would share some <em>personal opinions</em>, some which are self-evident and others that are purely philosophical.</p>

<p><img src="/assets/images/nix_pitch_fork.png" alt="nix pitch fork" /></p>

<p>Nix is brilliant <em>and</em> deeply flawed. Both things are true at the same time. The documentation is infamously terrible (although getting better!), the language is foreign to read for many and there are still growing pains from the governance changes. You should be free to say all of this and yet still believe Nix to be the best idea in the industry, at least this decade.</p>

<p><em>The emperor has no clothes.</em></p>

<h2 id="nix-isnt-for-everyone">Nix isn’t for everyone.</h2>

<blockquote>
  <p>“If we just fix the documentation and the onboarding, then there’s no stopping Nix &amp; NixOS” - <em>Most Nix users</em></p>
</blockquote>

<p>Nix is “having a moment”. Growth on any measurable metric is growing. <a href="https://gg-solutions.hashnode.dev/linux-the-silent-revolution">There</a> <a href="https://ryanrasti.com/blog/why-nix-will-win/">are</a> <a href="https://determinate.systems/blog/the-nix-moment/">endless</a> posts about it.</p>

<p><a href="https://gg-solutions.hashnode.dev/linux-the-silent-revolution"><img src="/assets/images/nix_growth_chart.png" alt="nix growth chart" /></a></p>

<p>Part of human nature is the desire to win. As a result, there is a persistent fantasy that if we <em>just</em> fix the documentation, <em>just</em> smooth the onboarding, <em>just</em> ship a friendlier installer and so on, then everyone, <u>even your grandmother</u>, can be using NixOS.</p>

<p><a href="/assets/images/grandma_nix_ifd_meme.png"><img src="/assets/images/grandma_nix_ifd_meme.png" alt="grandmother nix meme" /></a></p>

<p>Nix is not meant for everyone.</p>

<p>Nix is a power tool. Power tools have a learning curve and can occasionally take a finger. I continue to use a non-NixOS Linux machine in addition and guess what, it works well enough. It is surprisingly stable and despite what Nix leads us to believe, not everything is on fire.</p>

<p>Our obsession with mass adoption has warped priorities and diluted the amazing possibilities Nix could allow by having to make it more palatable for broader appeal.</p>

<h2 id="being-american-is-perceived-to-be-a-problem">Being American is perceived to be a problem.</h2>

<p>Nix originated in Europe. It should be no surprise that it skews heavily European. We can easily affirm this from the <a href="https://nixos.org/surveys/community/2025/#people">2025 community survey</a>.</p>

<p><img src="/assets/images/nix_survey_2025_geography.svg" alt="nix survey results" /></p>

<p>Europeans are different from Americans. We hold different cultural values and priorities. Both have traits that I wish the other emulated. Unfortunately, where they clash however is often a point of contention.</p>

<p>Americans are capitalist maximalists. The idea of the “American dream” is tied to it. We also have the largest military budget in the world. You’re rarely more than a degree or two from a through-line between a business and the military. This clashes with much of the European worldview.</p>

<p>As a result, there’s a bit of an undercurrent where being an American or an American corporation is problematic in the community. Your position is suspect from the start, assumed to carry ulterior motives and it curdles into a purity test.</p>

<h2 id="ai-is-useful">AI is useful.</h2>

<p>I am clearly in the “AI is useful” camp. I have <a href="/2026/05/31/ai-is-a-boon-for-the-anal-retentive">written before</a> about how much LLMs have unlocked for me personally.</p>

<p><a href="/assets/images/ai_is_slop_meme.png"><img src="/assets/images/ai_is_slop_meme.png" alt="ai is slop meme" /></a></p>

<p>The Nix ecosystem is probably the best poised for the AI-wave. I have found a newfound joy and love for my NixOS machine now with LLMs. All those weird quirks that bugged me I have been able to resolve, <strong>and declaratively reproduce</strong> for future generations. One-off AI written tools can be written and stored in my NixOS configuration with a sense of assurance that they will not collide or interfere with the rest of the system.</p>

<p>Unfortunately there’s a loud contingent that treats AI output at best as technically unsound and at worst as some moral failing of the user. Despite nixpkgs offering AI tools, an <a href="https://github.com/NixOS/nixpkgs/pull/534657">AGENTS.md</a> file was seen as heresy.</p>

<p>I particularly enjoyed Linus Torvalds, <a href="https://lore.kernel.org/linux-media/CAHk-=wi4zC+Ze8e+p3tMv8TtG_80KzsZ1syL9anBtmEh5Z40vg@mail.gmail.com/">on a kernel mailing list</a> articulating better than me Linux’s position on AI:</p>

<blockquote>
  <p>AI is a tool, just like other tools we use. And it’s clearly a useful one. […] Anybody who doubts that clearly hasn’t actually used it.</p>
</blockquote>

<p>These are tools we can use to push Nix &amp; nixpkgs further.</p>

<h2 id="bdfl-is-undervalued">BDFL is undervalued.</h2>

<p>The democratic process is great for society but a software project originated from <em>someone</em>. Someone had the vision, birthed the idea, worked tirelessly on it and then attracted others to contribute to their ongoing vision. In the case of Nix, Eelco created Nix in 2003 as part of his PhD research. The NixOS foundation didn’t exist until 2015. That is over a decade of work towards a project driven by his own vision as the <em>Benevolent Dictator for <s>Life</s></em> (BDFL).</p>

<p>A non-democratic model works especially well in open-source because you are free to fork the software and try your hand at your own ideas if you disagree – the same cannot be said for our shared geography.</p>

<p>A clear vision, whether you agree with it or not, is refreshing. Someone who can say yes or no and is not simply stonewalled by design-by-committee. DHH had said it poignantly well: <em>“Using open source software does not entitle you to a vote on the direction of the project.”</em> [<a href="https://world.hey.com/dhh/open-source-is-neither-a-community-nor-a-democracy-606abdab">cite</a>].</p>

<h2 id="abandon-macos-and-definitely-windows">Abandon macOS and definitely Windows.</h2>

<p>Every hour spent making Nix pleasant on macOS is an hour not spent making Nix <em>extraordinary</em> on Linux. You would never catch an iOS developer working on Linux and yet it pains me to see those who target the Linux platform working on a Mac.</p>

<p><a href="/assets/images/drake_macos_meme.png"><img src="/assets/images/drake_macos_meme.png" alt="drake macos meme" /></a></p>

<p>The single most-impactful advice I give to those earlier in their career is to use a Linux machine, especially if you build and write software for Linux (i.e. web stuff).
There are rarely any tips to fast-track your career beyond time except for this one.</p>

<p>For Nix, Darwin support is a bottomless tax. Closed toolchains, an SDK that shifts under you every release, a sandbox that fights you. All to chase an OS whose entire philosophy (opaque, proprietary, convention over purity) is the antithesis of Nix.</p>

<p>When we have to target solutions that cover wildly different platforms, the end result is muddled and limited. Beauty, elegance and innovation emerge when you apply constraints and restrict a problem.</p>

<h2 id="flakes-are-meh">Flakes are meh.</h2>

<p>Flakes are here to stay and yet their adoption is constantly <a href="https://determinate.systems/blog/experimental-does-not-mean-unstable/">brought up</a> in order to validate its existence. I default to using it for my new projects, mostly because it’s easier at this point and it seems annoyingly tied to the new CLI format.</p>

<p>Upon reflection though, I don’t feel like I have gained anything really over <code class="language-plaintext highlighter-rouge">npins</code> or <code class="language-plaintext highlighter-rouge">niv</code>. Subjectively my <code class="language-plaintext highlighter-rouge">nix</code> evaluations feel slower as now I’m fetching many more <code class="language-plaintext highlighter-rouge">nixpkgs</code> or jump through hoops to make every flake follow each other defeating the whole purpose of separate trees.</p>

<h2 id="single-user-install-is-great">single-user install is great.</h2>

<p>Unless you are sharing a laptop with your family or are using a mainframe from 1980, you won’t have more than a single user on your machine. Despite this, the default installation we guide users towards is one designed for multiple users.</p>

<p>The multiple-user install adds unnecessary complexity many don’t need or won’t understand: a build daemon, pool of <code class="language-plaintext highlighter-rouge">nixbld</code> users, systemd service, etc…</p>

<p>In contrast, the single-user install is radically simpler to run, operate and triage. I don’t have to remember whether I am setting configuration for my “client” or the “daemon” and that there is a difference.</p>

<p>Multi-user is the right default for a shared build farm. It is <em>overkill</em> for the single human it’s actually installed on the majority of the time.</p>

<h2 id="think-bigger">Think bigger.</h2>

<p>Nix is the closest thing we have to a solution where we can rebuild the entire world reproducibly. It is used by the Software Heritage Foundation as a way to reliably collect, preserve, and share all software that is publicly available in source code form [<a href="https://docs.softwareheritage.org/user/software-origins/nixguix.html">cite</a>].</p>

<p><a href="/assets/images/midwit_nix_meme.png"><img src="/assets/images/midwit_nix_meme.png" alt="midwit nix meme" /></a></p>

<p>Some of the most amazing technology that exists in this world exists when one can make changes at multiple layers throughout the stack. This is the <em>secret sauce</em> to many of the hyperscalers of today. This is table-stakes for NixOS. You can implement a solution that requires new: application, compiler, library, runtime and kernel all within a <strong>single commit</strong>.</p>

<p>Despite the ability to wildly diverge from traditional distributions, innovate and differentiate, we largely replicate the status-quo – albeit more reproducible. Whether it’s constraints imposed by needing to accommodate alternate platforms (e.g. macOS) or fearing alienating more novice-users we limit the potential of what Nix could do.</p>

<p>Are you pissed off? Let us still be friends.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Warning If you are pissed off reading this blog post, I guess, mission accomplished ? Try not to take life too seriously. Unfortunately, there’s a lot worse things in life than my opinions on Nix.]]></summary></entry><entry><title type="html">Who does Anubis actually stop?</title><link href="https://fzakaria.com/2026/07/09/who-does-anubis-actually-stop" rel="alternate" type="text/html" title="Who does Anubis actually stop?" /><published>2026-07-09T12:21:00-07:00</published><updated>2026-07-09T12:21:00-07:00</updated><id>https://fzakaria.com/2026/07/09/who-does-anubis-actually-stop</id><content type="html" xml:base="https://fzakaria.com/2026/07/09/who-does-anubis-actually-stop"><![CDATA[<p>I have been working on a patch to the Linux kernel to support <code class="language-plaintext highlighter-rouge">$ORIGIN</code> for the interpreter (<code class="language-plaintext highlighter-rouge">PT_INTERP</code>) via bpf in <code class="language-plaintext highlighter-rouge">binfmt_misc</code> [<a href="https://lore.kernel.org/linux-mm/20260622043934.179879-1-farid.m.zakaria@gmail.com/T/#t">thread</a>].</p>

<p>Of course I’m leveraging an LLM to help me do this! To pre-seed the context of the LLM, I asked it to read the <a href="https://lore.kernel.org/">https://lore.kernel.org/</a> thread.</p>

<p><a href="/assets/images/anubis-challenge.png"><img src="/assets/images/anubis-challenge.png" alt="Anubis challenge page" /></a></p>

<p>Uh oh. Looks like they have adopted <a href="https://github.com/TecharoHQ/anubis">Anubis</a>, which is an HTTP proxy that requires <em>proof-of-work</em> before allowing access to the resource.</p>

<p>Did this really do anything?</p>

<p>Unfortunately, no.</p>

<p>My AI diligently came up with <strong>anubis-fetch</strong>, which you can find at <a href="https://github.com/fzakaria/anubis-fetch">https://github.com/fzakaria/anubis-fetch</a>. The tool tries to natively solve the proof of work or, as a last resort, will launch Chromium to visit the URL.</p>

<blockquote>
  <p>This tool also impersonates a real Chrome TLS/JA3 fingerprint natively via <a href="https://req.cool/">req</a> so it clears passive Cloudflare blocking too. ☝️</p>
</blockquote>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># HTML to stdout
</span><span class="gp">$</span><span class="w"> </span>anubis-fetch https://lore.kernel.org/linux-mm/some-thread/T/
<span class="go">
</span><span class="c"># readable plain text
</span><span class="gp">$</span><span class="w"> </span>anubis-fetch <span class="nt">--text</span> https://lore.kernel.org/linux-mm/some-thread/T/
</code></pre></div></div>

<p>So who did we stop?</p>

<p>The exact adversary Anubis targets defeats it trivially.</p>

<p>The whole use of Anubis feels regressive and marginalizes those without access to “good” AI.</p>

<p>For a scraper, solving the Anubis challenge is a one-time, amortized-to-zero cost since the cookie can be cached and reused. For a human, it’s seconds of spinner, battery drain on every fresh visit. They can’t amortize anything amongst each other.</p>

<p>This “regressive tax” is paid even more so by those with weaker devices or who access the content on their phone. Clients that don’t leverage JavaScript (e.g., text browsers (w3m/lynx), screen readers, RSS readers) are completely left out.</p>

<p>Did deploying Anubis stop any of the aforementioned bot-farms or are they mildly inconvenienced when they had to augment their bots to support a new proof of work solution briefly?</p>

<p>The irony is that Anubis’s goal is to stop AI but it was incredibly easy for AI to circumvent it and yet the cost to humans and an open web remains.</p>

<p>With the presumption Anubis is now a regressive tax, how much does it cost us?</p>

<p>Every number here is a rough estimate. This is not a environmental argument at all since the bot-farmers and AI tools themselves are using many orders of magnitude more energy. Nevertheless, it’s interesting to see how much time is spent doing proof-of-work challenges that marginalize people.</p>

<blockquote>
  <p>Difficulty <code class="language-plaintext highlighter-rouge">d</code> is the number of leading zero <em>hex</em> characters the hash must have, so the expected work per solve is <code class="language-plaintext highlighter-rouge">W = 16^d</code> hashes.</p>
</blockquote>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Difficulty</th>
      <th style="text-align: left">Hashes / solve</th>
      <th style="text-align: left">Go native</th>
      <th style="text-align: left">Browser JS</th>
      <th style="text-align: left">Felt wall-clock</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>4</strong></td>
      <td style="text-align: left">65,536</td>
      <td style="text-align: left">~1.3 ms</td>
      <td style="text-align: left">~130 ms</td>
      <td style="text-align: left">~1–5 s</td>
    </tr>
    <tr>
      <td style="text-align: left">5</td>
      <td style="text-align: left">1,048,576</td>
      <td style="text-align: left">~20 ms</td>
      <td style="text-align: left">~2 s</td>
      <td style="text-align: left">~5–15 s</td>
    </tr>
  </tbody>
</table>

<p><em>Difficulty 4 is the common default. Rates assumed: ~50 MH/s native (Go), ~0.5 MH/s in-browser JS; “felt” wall-clock includes page load, the worker, and the reload.</em></p>

<p>Let <code class="language-plaintext highlighter-rouge">C</code> be the number of Anubis challenge-solves per day, worldwide. Assume a felt time of <code class="language-plaintext highlighter-rouge">t = 2 s</code> and device energy <code class="language-plaintext highlighter-rouge">E = 20 J</code> per solve (screen + CPU).</p>

<ul>
  <li><strong>Human-time / year</strong> = <code class="language-plaintext highlighter-rouge">C × t × 365 / 3.15×10⁷</code></li>
  <li><strong>Energy / year (kWh)</strong> = <code class="language-plaintext highlighter-rouge">C × E × 365 / 3.6×10⁶</code></li>
</ul>

<table>
  <thead>
    <tr>
      <th style="text-align: left"><code class="language-plaintext highlighter-rouge">C</code> (solves/day)</th>
      <th style="text-align: left">Human-time wasted / year</th>
      <th style="text-align: left">Energy / year</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">1 M</td>
      <td style="text-align: left"><strong>~23 person-years</strong></td>
      <td style="text-align: left">~2 MWh</td>
    </tr>
    <tr>
      <td style="text-align: left">10 M</td>
      <td style="text-align: left"><strong>~230 person-years</strong></td>
      <td style="text-align: left">~20 MWh</td>
    </tr>
    <tr>
      <td style="text-align: left">100 M</td>
      <td style="text-align: left"><strong>~2,300 person-years</strong></td>
      <td style="text-align: left">~200 MWh</td>
    </tr>
  </tbody>
</table>

<p>Collectively we are wasting an impressive amount of time waiting for access to websites; time we didn’t spend before the AI era. As a human, time is precious and finite to me, whereas to a robot it is not.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[I have been working on a patch to the Linux kernel to support $ORIGIN for the interpreter (PT_INTERP) via bpf in binfmt_misc [thread].]]></summary></entry></feed>