Latest Posts (20 found)
Farid Zakaria 4 days ago

Super Mario Derivations

One of the most surprising aspects of the Nix language is that it is lazy , especially if you have never used a lazy language before. This laziness is what makes much of Nixpkgs possible, and its complexity. One of the simplest ways to observe the laziness is by understanding that only the attributes you access are evaluated. The more whackier version of this is you can have endless recursion in an attribute set. Nixpkgs is filled with these bottomless attribute sets: The same store path every time. contains itself, and so does every package set inside it. 🤯 If laziness is what lets a recursive attribute set terminate, then the recursion doesn’t have to bottom out at all : That attribute set is infinitely deep. Indexing three levels into it costs exactly three levels of evaluation, and the rest of the infinite tree is never built because nobody asked. So an attribute path is a walk through a lazily-generated tree. Which made me wonder: what if the attribute path were input to something ? 🤔 I decided to take that idea and make the attribute path a sequence of button presses in Super Mario Bros. 3 . Each node in the tree is a frame of the game, and each child is a button press that produces a new frame. Game states are recursive by nature. is right + B, which in Super Mario Bros. 3 is “run right”. is run and jump. The output is the frame you’d be looking at if you’d pressed those buttons in that order, on real hardware, in that game. 1 Append anywhere along the path and you get the whole run stitched into a recording: The coolest thing though is that every one of those frames is a separate derivation in my store . The code is at fzakaria/nes-nix . It is generalized and the ROM is a flake input you point wherever you like for any other game. The flake computes a derivation based on the attribute path such that each press is its own derivation, and it takes the previous press’s savestate as an input . Each derivation never re-emulates its ancestors’ frames. 2 The practical consequence is that the store becomes the emulator’s savestate history: Branching off the middle of a hundred-press run costs one press as does appending to the end of it. We can look at it the other way. The dependency graph is the input sequence, so we can ask Nix what buttons produced a frame: So what is actually doing? Almost nothing. Every frame along the path is already sitting in the store as the output of its own press, so the recording never emulates anything. It is a directory of symlinks to the frames for to process. How far can we take this input-sequence game input idea? Nix by default gives out at around 2,400 presses, with: defaults to 10,000 and evaluating each press costs roughly four nested calls. It’s a guard against runaway recursion, not a structural limit, and we can raise it to 10 million and get 20,000 presses: 20,000 presses, takes roughly fourteen seconds to evaluate on my laptop. The cost is linear in the number of presses, and it is roughly 0.7ms “per press”. The next bottleneck though is that the kernel gives out at 21,845 presses on my machine. An attribute path is a single element, and Linux caps the size of the argument list in total and individual arguments. The per-argument limit is 131,072 bytes ( ), and each press is six bytes long ( ), so 21,845 presses is the maximum that can be passed to as a single argument. The escape hatch is to stop passing the run as an argument. and we can feed in the input-sequence as from a file: This produces the byte-identical derivation to the equivalent attribute path, so a run kept in a file still shares the same store paths. All of this was to simply evaluate the Nix expression. Now we have to build it. Although Nix is great at building derivations in parallel, the recursion here is tail-recursive and therefore serial. I benchmarked the build time of a growing list of button presses and the cost is also linear, as we would expect, with the number of presses. The cost per press is roughly 1.27 seconds with substituters enabled and 0.28 seconds with them disabled. The round-trips cost for checking whether the derivation is in the cache costs noticeably more than emulating the frames does. 3 We’re used to the attribute path being a name , simply a coordinate into a catalogue of things that exist. Laziness means it’s really a program : a sequence of steps the evaluator walks, generating whatever it needs as it goes. Nixpkgs happens to use that machinery to describe software, but nothing about it requires that the tree be a catalogue at all. Coupled with the fact that the store turns out to be a decent persistence layer for reproducible state-machines, makes a our “package manager” reasonable to use for playing Mario. 🍄 The prefix is a precanned sequence of button presses that gets you to the start of level 1-1.  ↩ A screenshot of the frame is also produced, which is used when we want to stitch a video sequence together.  ↩ We can set or if we want to avoid this cost.  ↩ The prefix is a precanned sequence of button presses that gets you to the start of level 1-1.  ↩ A screenshot of the frame is also produced, which is used when we want to stitch a video sequence together.  ↩ We can set or if we want to avoid this cost.  ↩

0 views
Farid Zakaria 1 weeks ago

A C++ toolchain from 357 bytes, in Bazel

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. What is stage0? It is a chain of compilers and assemblers that can be built from source, starting from a 357-byte program that can eventually build a recent GCC. 1 Since then, NixOS and other distributions have also adopted the same approach to minimize their binary seed which makes it possible to onboard new architectures and platforms much simpler. What’s always frustrated me as a Bazel (& Buck ) user is the reliance on prebuilt toolchains even for things that should be built from source easily like protoc . Bazel has given up trying to provide a hermetic C++ toolchain and the upstream rules_cc ruleset just points you elsewhere: Configuring a hermetic toolchain makes your build more deterministic. rules_cc itself does not yet offer a hermetic toolchain distribution I had attempted to provide a stage0 hermetic C++ toolchain in October 2024 via https://github.com/fzakaria/stage0-bazel . I made substantial process through the bootstrap process but I did not make it far enought to be usabale. 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 MODULE.bzl madness . 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. 2 You can now leverage the toolchain to build in Bazel and have it compiled by a toolchain whose entire ancestry is in the repository from that same 357-byte seed . 🎆 How complete is this toolchain? I pointed the toolchain at Abseil and GoogleTest straight from the Bazel Central Registry without any patches . We then can build and run their testsuite to provide a sanity check that the toolchain is working correctly. We use a to filter tests that require . Abseil marks as a , and Bzlmod drops dev dependencies of non-root modules. That is us building Abseil and GoogleTest, from the registry, unpatched, compiled by a toolchain that began as 357 bytes of hex. How can I be so sure this is a hermetic toolchain? The toolchain includes an audit report that uses Bazel’s aspects 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 and will fail if any action executes a program outside of the Bazel output tree. 3 The report is two lines long: Unfortunately, since runs a shell it takes as an absolute system path that is also listed as a seed binary. ’s attribute is a string, and the shell is not a declared input of the action, so no artifact this repository built can provide it. 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. Once you can reach a recent-enough GCC, you can build any C/C++ program and beyond easily.  ↩ Consider this the disclosure that I used an LLM to help me write the remainder of the toolchain.  ↩ We also set to disable Bazel’s built-in C++ host toolchain detection.  ↩ Once you can reach a recent-enough GCC, you can build any C/C++ program and beyond easily.  ↩ Consider this the disclosure that I used an LLM to help me write the remainder of the toolchain.  ↩ We also set to disable Bazel’s built-in C++ host toolchain detection.  ↩

0 views
Farid Zakaria 1 weeks ago

Nix finally has a source-bootstrapped OpenJDK

One of the earliest requested issues we had opened on GuixPkgs was to add more packages, specifically OpenJDK. “Specifically I would like to see openjdk translated. openjdk is not bootstrapped from source code in nixpkgs” [ issue#3 ] Ever since I learned about the stage0 bootstrap chain and how Guix announced full-source bootstrap for all packages 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. 1 GuixPkgs can now build OpenJDK 25 🎉 Does it really work? Let’s take it for a spin. lives in the output, since Guix splits the package. is written in Java. 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. Every distribution resolves this the same way in practice: download a JDK and use it to build your JDK. Debian documents the pain , and the Bootstrappable project has a whole page 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. Nixpkgs does the same. is built by : a 135 MB prebuilt-tarball. Nixpkgs makes it pretty easy to audit in the meta.sourceProvenance of the package. What does the source provenance of GuixPkgs’ look like? It’s a little whacky but the overall build chain in Guix is the following: Nineteen complete JDK builds, from a C++ program. We can use to emit the entire closure as a dot file to visualize the differences. 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. The upper panel’s entire Java story is those two red dots and the one edge between them: → . The lower panel’s is the 33-node constellation. Why is the Nix graph still so big if I claimed it was from a binary distribution? It turns out that a JDK closure is mostly not Java. 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. 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. Which derivations exist in this closure only because of how the JDK is bootstrapped? 🤔 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. This lines up with our intuition. Nixpkgs only needs two derivations to bootstrap the JDK, while GuixPkgs needs 876 . Note Interestingly, the bootstrap build for JDK has in its closure: IcedTea 8 wants GTK 2, which wants its own Mesa, which wants 😱 What started off as a fun art project , started during TacoSprint 2026 , has now found relevance to those interested in bootstraping and reproducible builds. 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.  ↩ jikes 1.22 (C++) compiles GNU Classpath 0.93 , a free reimplementation of the Java class library. Classpath is enough to bring up JamVM 1.5.1 , a JVM written in C. Now something can run Java. Now we can finally use Java tools: Ant and ecj , the Eclipse Compiler for Java. Then it doubles back and rebuilds all of that with the newer versions to get more modern JDK features. That stack finally compiles IcedTea 2.6.13 (OpenJDK 7) → IcedTea 3.19.0 (OpenJDK 8) → OpenJDK 9 . After which it is one rung at a time: 9 builds 10, 10 builds 11, …, 24 builds 25. 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.  ↩

0 views
Farid Zakaria 1 weeks ago

Guix by Nix

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: We can then build a NixOS machine where every package is the Guix equivalent 😱. Well, adeci 1 took the bait and we went even further than that. 😈 Say hello to Guix by Nix : a bootable VM where the kernel is Guix’s Linux-libre, the userland is translated Guix packages, and PID 1 is GNU Shepherd . No systemd. No D-Bus. No NixOS activation. Not even a or binary in the guest. It’s Guile all the way down and Nix built all of it. Log in with / and poke around: As a reminder, even though these binaries live in , they are not Nixpkgs packages. They were translated from Guix derivations using guix-transfer and built by the . That was built from Guix’s package definition, source bootstrap and all , but it lives in , because built it. If you want to try out any of these packages on your own machine, you can use GuixPkgs . Tip 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 whacky steps to get there. How does this actually work? The project is a three-stage pipeline: Warning AI was leveraged to write the initrd and activation scripts. That seems to trigger people lately, so consider yourself warned. 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, . This is Guix by Nix . “Every executable byte comes from Guix” is exactly the kind of claim that’s easy to say and easy to fudge. 🤥 A booted demo is great but that can’t prove it. A VM where secretly came from Nixpkgs boots identically . The flake ships an audit-check that classifies every store path in the shipped closure by provenance, unpacks the compressed initrd 2 , 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 reference survived translation, or anything systemd-shaped appears anywhere. 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. 🕵 Note The kernel is Guix’s , translated and built by 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 is byte-for-byte Guix’s. One obvious next step would be a normal NixOS machine, where every package that exists in GuixPkgs shadows its Nixpkgs equivalent on . GuixPkgs offers such an overlay, but it needs quite a lot of CPU to build all of it…. 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 sixos . 🫠 For now, this project remains a minimal VM demo. If you make it PID 1 on real hardware, please send photos. 🙇 It has been extremely fun and rewarding to work with Alex on this project. We nix-pilled him at PlanetNix 2 years ago and since then he has been pushing the boundaries of what Nix can do and is currently employed at Shopify working on Nix.  ↩ Nix’s reference scanner can’t see inside archives and sneaky paths hide there.  ↩ guix-transfer is the tool that translates Guix derivation graphs into Nix derivations. GuixPkgs is the flake with all Guix packages, all built from the 357-byte seed, although there is a Cachix cache provided. Guix by Nix assembles ~42 of those packages, a subset of the overall set, into a useable machine. The initrd is a custom shell script, interpreted by Guix Bash, that loads eight modules, mounts the root disk and the 9p store, and s. Activation (accounts, , the setuid copy) is another custom Nix-generated script run by Guix Bash. PID 1 is from Guix, with a small Scheme config that starts , , , and a serial . works like you’d expect. is compiled from Guix’s own search-path specifications, so and friends point where Guix intended. It has been extremely fun and rewarding to work with Alex on this project. We nix-pilled him at PlanetNix 2 years ago and since then he has been pushing the boundaries of what Nix can do and is currently employed at Shopify working on Nix.  ↩ Nix’s reference scanner can’t see inside archives and sneaky paths hide there.  ↩

0 views
Farid Zakaria 2 weeks ago

Seriously, what is the large code-model even for?

I have been working on making massive binaries possible at . One of the Hail Marys that you should be able to rely on is the large code-model ( ) as it makes no assumptions about size and distance of relocations. Large code model : The large code model makes no assumptions about addresses and sizes of sections. [ cite ] In a previous post , 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. 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 ( / ) being one of the “fun failure modes”. Turns out that it is worse than I thought. The instruction sequences the compiler emits for TLS are 32-bit by construction , and has nothing to swap them for. 🤦🏻‍♂️ To put that very bluntly, is incapable of building complex large binaries despite its stated goal. The annoying part of this whole line of research is producing a test subject. Emitting 2GiB of real instructions into 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. But relocation overflow isn’t about how many bytes are on disk, it’s about virtual-address distance between a reference and its target. So we have a few options. Uninitialized globals land in , which is a section: it occupies virtual address space but zero bytes on disk (the same sparse-file trick I wrote about in massively huge fake files ). So a few thousand 1MiB arrays gives us gigabytes of address space essentially for free. We can synthetically produce this pretty easily with the following script. Link it with the (default) small code-model and it falls over exactly where you’d expect, at the 2GiB signed-32-bit boundary: sits at = precisely 2GiB. Recompile the same source with and it links cleanly: The large model did its job: it replaced the 32-bit references with 64-bit sequences: a of a 64-bit offset + an . The 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 segment. We can leverage the assembler’s directive to repeat instructions. We can generate a tiny source of N functions spaced 1MiB apart with a sea between them, plus one dispatcher that s every function. This requires each to have a relocation and the calls to functions past the 2GiB mark overflow. 🔥 Now let’s do the exact same thing, but using . When you access a thread-local variable, the compiler doesn’t just load an address, it emits one of four access models , from fastest/least-flexible to slowest/most-flexible: Notice the pattern: the value that reaches the thread pointer can be 64-bit (i.e. it lives in a GOT slot), but every instruction that participates in a TLS access uses a 32-bit field . , , , are all 32-bit. 4GiB of , still tiny on disk. Now look at the relocations the large code-model generated: Every single TLS access is which is 32-bit , even under . 🫣 The exact same large code-model that just happily linked 4GiB of ordinary fails on 4GiB of . This isn’t a GNU quirk either. LLVM ( & ) does exactly the same thing, albeit with ’s diagnostic being a bit friendly by printing the actual offset and the window it has to fit in: is exactly , sitting at the far bottom of the TLS block, and is the signed 32-bit window has to live in. even parks large-model code in a section. I was a little confused at first: exists . Why doesn’t the compiler use it, especially with ? A relocation type is glued to the specific field it patches and is determined by the code-sequence emitted by the compiler. Here’s the local-exec sequence emits: The offset is applied with . The x86-64 encoding for that instruction has only a 32-bit immediate field . There is no 64-bit form of it. The only relocation that can physically patch that field is a 32-bit one: . So where is used? It’s not in the code ( ) at all. It lives as a dynamic relocation on the GOT slot used by initial-exec . The code side only holds the 32-bit that points at the slot: patches the 8-byte (64bit) data word in the GOT. The instruction that reaches that word ( ) 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. 😭 local-exec is the mode you want 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 with no memory load, no GOT, no indirection. Unfortunately, is limited to 32bits and overflows. I would argue it’s also the mode most likely to need a large binary, since it’s what a giant static executable uses for its own TLS. What if we fall back to a slower mode? Unfortunately, each one of them has a 32-bit field too: The use of 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. 🥲 After having dug into it, it’s not simply a bug in GCC or LLVM. Look again at what emits for a single local-exec access under : It’s a plain . There is nothing stopping clang from emitting a 64-bit form instead: could patch that immediate, exactly the way already patches a immediate for ordinary data. Oddly, the relocation type is not the missing piece. What’s missing is a code sequence in the x86-64 psABI that uses a 64-bit TLS offset as an instruction immediate. The ABI simply never defined a large code-model TLS access model, so / only ever appear in GOT slots, never in code, and no toolchain can emit what the spec doesn’t describe. 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. 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 x86-64-abi google-group where I’ve posted an RFC, and we have started an LLVM Massive Binaries working group and hold monthly meetings. initial-exec is 32-bit RIP-relative reach to the GOT slot. general-dynamic is 32-bit RIP-relative.

0 views
Farid Zakaria 2 weeks ago

Small site changes and new design

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. 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. If you think the site looks much worse, please let me know . Some small improvements and quality-of-life enhacements: Look at me! I’m sidebar content I will undoubtedly abuse. I continued to try and give it my personal flair to avoid it looking like AI-slop but 🤷 A small entry in the footer to showcase the site is built with Nix. A Jekyll plugin that automatically creates source-sets for the images I use in my blog posts. A Jekyll plugin to create a short content hash for the stylesheets to avoid the browsers caching stale CSS. A Jekyll plugin that generates a little curved line for the masthead that is the of the page. Redesigned the layout to allow side-bar content.

0 views
Farid Zakaria 2 weeks ago

Linux kernel will support $ORIGIN, sort of

For some reason, during TacoSprint 2026 I decided to see if we could tackle relocatable binaries in Nix. I enjoy these lofty goals to push Nix and the surrounding ecosystem forward. I am bold if not stupid . I left the last earlier post with one potential idea of how to get there: We could patch the Linux kernel so that $ORIGIN is supported in PT_INTERP and the shebang. 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. My first attempt here proposed simply adding direct support for in the Virtual File System (VFS) subsystem. I waited nervously. I was expecting the result from what I had come to read about online; someone non-politely telling me to F$#CK OFF because there is something I missed, misunderstood or did not consider. 🤬 The result was completely different. 😲 Christian Brauner , 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. Note It definitely helped having someone like John Ericson chime in and advocate why having a non-fixed interpreter ( ) is useful to Nix and other use-cases (i.e. Buck & Bazel). He offered that potentially we could leverage eBPF as a programmable way to select an interpreter through binfmt_misc . I wanted to merely allow but a programmable selection could let us do anything! The idea must have really intrigued him because soon-after, on his vacation , 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 patch series that will make its way into branch in the near future. If you don’t know what eBPF is or , WTF did we just collaborate on? Let’s take a look! I won’t do eBPF justice, and there are plenty of articles online about it as it’s quite in-vogue at the moment. tl;dr; You can write programs in a C subset that gets compiled to an instruction set whose virtual machine is running within the kernel . 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 this guide for more info. We can now support with a relatively simple eBPF program: Once the above program is loaded and registered into the kernel, we then ask the subsystem to trigger it. Checkout this thread if you want to see the complete example. What does that mean? It means that every binary now triggers the function above, in this case any file, but it could be executables with a new segment like , and the kernel will ask to determine the interpreter to use dynamically. Our special BPF program has support for 💥 What else could you do? Well we can now even completely replace the traditional QEMU registration script with a BPF program now like this one . What else can we do? Since we can now programmatically select our interpreter based on anything in the file, we can do quite a lot. I’m keen to hear your suggestions and ideas 💡. Some of the smaller items are that we can even support in the shebangs ( ) very easily as seen here : we simply look at the first 256 bytes of the file and look for to trigger. One downside or side-effect of the traditional hand-off was that the way in which the desired final binary was invoked was non-transparent . The registered interpreter becomes the process. It owns the entire process identity, and the binary you actually asked to run gets demoted to an argument. For or that’s acceptable as they are emulators but for a per-binary BPF loader that might pick a traditional it does not make much sense. This leaks in a few painful ways but the simplest are : Christian sent a large patch series for this as well. His latest patch series adds two new dispatch modes that close the gap from opposite ends and covers a few other gotchas that these modes can fix. The loader substitition is the one I’m most excited about for Nix. With the flag, the kernel executes the matched binary natively as the main image, and merely substitutes the registered interpreter for the loader named in the binary’s . stops being a hand-off and becomes a plain override. There’s no contract and no identity to reconstruct, so a stock dynamic loader works unchanged . Where does this leaves us? I’ll be tracking the Linux kernel releases and, once this lands in and ships in a tagged release, I plan to upstream a NixOS module that registers the support at boot. 🎉 The plan is to gate it on a new segment rather than matching every file. That keeps things backwards compatible : 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 relocatable status . A ship in harbor is safe, but that is not what ships are built for. — John A. Shedd and show the interpreter invocation, not what you executed. names the interpreter. Relocatable programs commonly locate themselves through , and instead they find the dynamic linker. 😩

0 views
Farid Zakaria 3 weeks ago

How to piss off your Nix friends

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. Seems like it’s all too easy to get people in Nix flustered, angry and out with their pitchforks. All it takes is someone proposing a markdown file for the community to lose its mind. Having been a member, exposed to and part of the Nix/NixOS community for many years, I thought I would share some personal opinions , some which are self-evident and others that are purely philosophical. Nix is brilliant and 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. The emperor has no clothes. “If we just fix the documentation and the onboarding, then there’s no stopping Nix & NixOS” - Most Nix users Nix is “having a moment”. Growth on any measurable metric is growing. There are endless posts about it. Part of human nature is the desire to win. As a result, there is a persistent fantasy that if we just fix the documentation, just smooth the onboarding, just ship a friendlier installer and so on, then everyone, even your grandmother , can be using NixOS. Nix is not meant for everyone. 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. 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. Nix originated in Europe. It should be no surprise that it skews heavily European. We can easily affirm this from the 2025 community survey . 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. 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. 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. I am clearly in the “AI is useful” camp. I have written before about how much LLMs have unlocked for me personally. 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, and declaratively reproduce 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. 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 AGENTS.md file was seen as heresy. I particularly enjoyed Linus Torvalds, on a kernel mailing list articulating better than me Linux’s position on AI: 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. These are tools we can use to push Nix & nixpkgs further. The democratic process is great for society but a software project originated from someone . 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 Benevolent Dictator for Life (BDFL). 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. 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: “Using open source software does not entitle you to a vote on the direction of the project.” [ cite ]. Every hour spent making Nix pleasant on macOS is an hour not spent making Nix extraordinary 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. 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. 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. Flakes are here to stay and yet their adoption is constantly brought up 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. Upon reflection though, I don’t feel like I have gained anything really over or . Subjectively my evaluations feel slower as now I’m fetching many more or jump through hoops to make every flake follow each other defeating the whole purpose of separate trees. 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. The multiple-user install adds unnecessary complexity many don’t need or won’t understand: a build daemon, pool of users, systemd service, etc… 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. Multi-user is the right default for a shared build farm. It is overkill for the single human it’s actually installed on the majority of the time. 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 [ cite ]. 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 secret sauce 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 single commit . 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. Are you pissed off? Let us still be friends.

0 views
Farid Zakaria 1 months ago

Who does Anubis actually stop?

I have been working on a patch to the Linux kernel to support for the interpreter ( ) via bpf in [ thread ]. 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 https://lore.kernel.org/ thread. Uh oh. Looks like they have adopted Anubis , which is an HTTP proxy that requires proof-of-work before allowing access to the resource. Did this really do anything? Unfortunately, no. My AI diligently came up with anubis-fetch , which you can find at https://github.com/fzakaria/anubis-fetch . The tool tries to natively solve the proof of work or, as a last resort, will launch Chromium to visit the URL. This tool also impersonates a real Chrome TLS/JA3 fingerprint natively via req so it clears passive Cloudflare blocking too. ☝️ So who did we stop? The exact adversary Anubis targets defeats it trivially. The whole use of Anubis feels regressive and marginalizes those without access to “good” AI. 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. 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. 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? 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. With the presumption Anubis is now a regressive tax, how much does it cost us? 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. Difficulty is the number of leading zero hex characters the hash must have, so the expected work per solve is hashes. 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. Let be the number of Anubis challenge-solves per day, worldwide. Assume a felt time of and device energy per solve (screen + CPU). 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. Human-time / year = Energy / year (kWh) =

0 views
Farid Zakaria 1 months ago

A TacoSprint 2026 Retrospective

This is my retrospective of TacoSprint 2026 that took place in June 2026 at La Saladita, Guerrero, Mexico. For a while now, I have watched from the sidelines as Nixers around the world gathered for sprints: OceanSprint , ThaigerSprint , SaltSprint , TransylvaniaSprint , AuroraSprint and NixCamp . Wow, we Nixers sure do like our sprints! All of them also happen to be in Europe or East-Asia. With an upcoming fourth baby on the way, I figured it was now-or-never to put words into action. I messaged @domen , who organizes OceanSprint every year, and asked if he’d be interested in helping me set up the first sprint in North America. Domen is an avid surfer, a recurring theme in his OceanSprints, so I appealed to his inner-surfer and we spec’d out some places in North America that were both cost-effective and had ample, amazing surf 🏄. I had already been to the Troncones and the La Saladita area, so my prior experience removed a large vector of the unknown. It seemed like a no-brainer. Domen was already going to be in South America in June, so the timing lined up nicely (+ summer is the swell season there!). We set to work standing up a website and trying to attract sponsorship and attendees. This was probably the hardest part of organizing a brand-new sprint. We had far lower turnout for registration and sponsorship than I foresaw. Several people responded on our application form, or told us directly, that they were unsure about the safety of visiting Mexico, since the US Department of State had it under a travel advisory. Despite my best efforts to soothe everyone’s fears, it remained a real hindrance. Note For those still on the fence for next year: the area felt extremely safe. We rented a house in a fairly secluded stretch that caters almost entirely to surfers. At no point did anyone feel uncomfortable or unsafe. Getting there was its own small adventure. Flights were unusually challenging to book thanks to the World Cup soaking up demand across the region. The most dramatic casualty was Alex ( @adeci ), who managed to completely miss his connecting flight and arrived three days late. To his credit, he showed up in great spirits and slotted right back in to hacking with the group like nothing happened. Once everyone was settled, we fell into a rhythm that I can only describe as suspiciously sustainable : It was amazing to bookend each day with a surf at La Saladita’s left point break. Surfing for me lets me enter flow state very similar to when I am deep in thought hacking-away. It helped clear through a lot of built-up gunk and I often returned back with a clear intention or solution to a problem I had been working on. One of the more unexpectedly wonderful parts of the trip was the meal preparation from Gladys, our local cook, who pretty much cooked for us three times a day. We were extremely well-fed, which let us focus on the Nix-hacking and motivated me to make sure I kept up with the surfing to put off any weight gain 🫠. The website will be updated to have a more formal summary of every contribution we managed to put forward and their current status however it was amazing to see how much work a group of nine people can put forward in a single week with a combined mission and passion for an ecosystem. Our work spanned dynamic linking, package relocatability, peer-to-peer remote builds, faster module systems, shrinking the OCaml runtime closure and cross-distribution packaging. A few of my own threads, if you want to go deeper: LLM-based agents featured prominently throughout. We were fortunate to have Geoff Huntley with us, who is quite the AI-maximizer , spiritually guiding us and offering us some SOTA insight in how we might want to explore leveraging AI. Alan ( @alurm ), had the greatest idea for us to put together an academic style trip report. We worked together on the paper and the result is Attention, Nix and Tacos Is All You Need , a loving parody of a certain famous paper. An arXiv submission is coming, but in the meantime you can read it below or download it here . Your browser doesn't support embedded PDFs. You can download it here instead. We already agreed to organize the same sprint next year. I can’t wait. This was literally the most enjoyable thing I’ve ever done as it combined my two passions (surfing & hacking) in a way I honestly did not think was possible all while producing a ton of value to the Nix ecosystem. For a different vantage point, please check out the retrospectives from my fellow attendees! GuixPkgs: every Guix package, as a Nix flake Hijacking ELF entry points for NixOS compatibility, or wtf is wrap-buddy Nix needs relocatable binaries Alan Urmancheev Jared Siegel

0 views
Farid Zakaria 1 months ago

Hijacking ELF entry points for NixOS compatibility or WTF is wrap-buddy?

We are part-way through TacoSprint 2026 and a project that has inspired me has been the long-standing pursuit of producing relocatable binaries in Nix. This is something I’ve been discussing since as early as 2022 . We’ve made pretty great headway! 🥳 I posted a proposal to the Linux kernel mailing list to add support for to , which will allow for resolving the interpreter relatively. I also submitted PR#534339 to nixpkgs which improves the generation and shrinking by modifying them to leverage as well. This needs no new Linux kernel support and will make Nix derivations a teeny bit more relocatable. Throughout this investigation, I was informed about similar efforts via wrap-buddy by the venerable Mic92 . I opened the GitHub project and I have to admit, I did not quite understand it. Jörg is an amazingly prolific and technical developer, and despite my knowledge of the space, it took me a while to understand the craziness beauty of what was being done. So, wtf is wrap-buddy ? Nix is all about explicit dependencies and it leverages this with techniques like on the ELF binary. This all works for newly minted code, but if you try to download any precompiled binary on your NixOS machine, you’ll hit an error for a myriad of reasons. One of the biggest being that the dynamic linker/interpreter, , does not exist on NixOS. We would love to compile everything from source, but the reality is that plenty of software people want to use is closed . In order to allow that to work on NixOS machines, derivations may patch the ELF files with patchelf setting things like and to Nix-friendly paths. In some rare cases, however, that doesn’t work. The documentation in claims: autoPatchelfHook can be error-prone and may break binaries that, have unusual ELF layouts. In these pathological cases, is an alternative that takes over the startup of the binary to modify it at runtime. 🤯 Let’s take a look with a small example. We can build a small C program linked against two shared libraries, and , forcing a non-NixOS interpreter path: If we run this binary, it fails immediately because doesn’t exist or it can’t resolve . Now we patch it using pointing to our library paths: Now if we run our binary, , we see that it works: What did it do? 🤔 First off, it copies the first 416 bytes of our program code into a hidden file named . Let’s peek at the original binary and the instructions for : saves those starting 416 bytes to the hidden file . The configuration file format starts with a 22-byte header, followed by the interpreter string (83 bytes) and string (442 bytes), placing our saved original instructions at offset 547 ( ): Next, it clears our to so the Linux kernel thinks it’s a statically linked binary and boots it directly: Lastly, it overwrites our entrypoint with that small stub (416 bytes). We can see in the disassembly that immediately redirects and calls now: Why all this complexity? What is doing? The goal of is to find a known custom loader, , which will help us finish all the dynamic linking. The custom loader gets even more nuanced and low-level. It would be a disservice to try and completely go over everything it does, and at this point the README does a fairly good job. At a high level: The NixOS dynamic linker takes over, uses the to resolve and . We can now run the application using the restored original entry point with everything resolved. Magic. Wizard. Mic92 . 🧙 It reads the saved original bytes from the file and copies the original bytes back over our stub in memory. To any observer, the binary is now completely clean and resembles the original. It injects the custom by creating a brand new dynamic section in memory and populates it with the containing our library search paths that we stored in . It loads the real NixOS interpreter into memory. It rewrites the kernel’s stack metadata (auxiliary vector pointers like , , and ) to trick the native loader ( ) into believing it was loaded natively by the kernel. Finally, it jumps to the entry point of the NixOS interpreter.

0 views
Farid Zakaria 1 months ago

Nix needs relocatable binaries

This is my problem statement and proposal for a TacoSprint 2026 project 🏄. Nix, or store-based systems , are a class of package managers that use a well-defined prefix to store all packages. This can be for Nix or for Guix. This is simple. It makes rewriting paths to binaries or libraries easy. Derivations only need to the strings with the full store-path; becomes for instance. What if you wanted a different path, one not prefixed at the root ? This could be desirable if you don’t have Nix installed already or are missing necessary permissions – “rootless Nix”. Well, Nix already lets you specify a different store-path today but there is a catch! Let’s take a look at a simple example. We can build two different ways. The first command builds and installs at and the second at using and mount namespaces. Notice both have the same hash . This is important. By keeping the hash the same, we can leverage the precomputed derivations from binary substituters like https://cache.nixos.org . Ok, so what’s missing? If you are using tools like Bazel or Buck2 they likely already employ their own sandboxing via namespacing for builds. Integrating Nix into these ecosystems becomes incredibly impractical because we run into nested user namespace and mount restrictions. We can ask to use an alternate store prefix, without chroot and mount namespaces but it has a big gap. The hash is now 😭 It’s even more disastrous. Changing this simple string cascade-invalidates the entire dependency graph. You are now waiting 4 hours for GCC to compile just so you can print “Hello World” from a different folder. 🫠 This means we cannot leverage the public cache. This gap is called out by the Nix documentation today. Does it have to be that way? What if we could install Nix binaries anywhere , without using namespacing or . Can we have our cake and eat it too? 🍰 Nix needs relocatable binaries . The problem is that the store-prefix is part of the derivation itself so it affects the hash calculation. We don’t have to specify the full store-prefix everywhere. What if we used relative paths ? 🤔 Let’s look at one place the full paths are written today in the binary via . When this program runs, the dynamic linker looks at to find its shared dependencies. The loader in Linux however natively supports the variable which translates to “the directory containing the executable.” [ ref ] We could instead write the to be . If we did that then changing the store would cause no hashes to change. No recompilation. 🥳 Okay, so are we done? Well, like most things the devil is in the details. 😈 Before the dynamic linker can read the to find the necessary libraries, the Linux kernel has to load the dynamic linker itself. This path is stored in a different ELF header called (Program Interpreter). Unfortunately, the Linux Kernel does not support in this field as of today . We run into the exact same kernel limitation with the shebang line in scripts as well. When we execute a script, the kernel parses the (shebang) and expects an absolute path. Support for is also lacking as as of today . We cannot use relative paths reliably here unless they are relative to the current working directory, which breaks the moment you run the script from anywhere else. To achieve true relocatable binaries, we need to bypass these kernel limitations. historically would never make sense for in the Linux kernel because “Why would you want your dynamic linker to be found relative to the file!?”. Nix has changed that assessment. There are a few ways we could attack this: I believe augmenting support in the Linux kernel is the right approach. The beauty of Nix is we can even patch the kernel today in any NixOS machine for this support. As a final cherry on top, we can include additional metadata on every derivation whether it’s relocatable . 🍒 We could patch the Linux kernel so that is supported in and the shebang. We wrap every binary with a small static binary that computes its own location and then invokes the dynamic linker. We need to replace file locations to also leverage language-specific features for relative paths. For instance, in Python we can leverage to access files relative to itself similar to .

0 views
Farid Zakaria 1 months ago

A trillion dollars

This is not meant to be a political post. Headlines recently have been projecting Elon Musk’s net worth to hit $1 trillion USD. Working in software, you inevitably come up against Jeff Dean’s latency numbers every programmer should know . The original post wasn’t just insightful for sharing the time costs of common I/O operations. By using the scale of one access pattern to contextualize the next order of magnitude, it really helped entrench the cost of these increasing access patterns in my mind. Scale at increasing orders of magnitude is often difficult to comprehend. Although logarithmic graphs are useful for showing exponential growth or displaying vastly different scales on a single chart, they are easily misunderstood. If we were to map 1 million USD to 1 ns , what are the matching parallels to I/O access patterns? I have seen similar graphics for representing wealth, but I decided to make my own – because why not . Check it out: https://fzakaria.github.io/trillion-wut/ – how fast can you scroll to the bottom? You can find the source available at https://github.com/fzakaria/trillion-wut .

0 views
Farid Zakaria 2 months ago

Relocatable Derivations

The earlier post on guix-transfer highlighted how we can use the tool to transfer derivations from to . It is always delightful when someone offers deeper insights into an idea I had put forward. I was so focused on the transfer from derivations from Guix, I failed to see the larger applicability of the tool. @tomberek shared with me the insight that the tool can be generalized to: “transfer derivations between realms” . Relocatable derivations. 💥 What does that mean? Perhaps the clearest concept to apply it to are deployments . You might have a derivation that you want to propagate through cascading deployment tiers: alpha, beta, and prod. You might have needed to painstakingly apply some logical firewall if all three realms used as their prefix to gate your deployment. By changing the prefix of each one, i.e. or , they are naturally segregated in Nix. How do we promote derivations from one realm to another? We could re-evaluate the Nix expression again against each new store or we can leverage guix-transfer . Why is this better than doing against these new store directories? What is our source of truth? The Nix files or the derivations? I posit that the derivations themselves are the source of truth. Furthermore, evaluation could be slow and requires the full source code (Nix expressions) and the entire evaluation environment (i.e. Nixpkgs, plugins, and overlays). By relocating at the derivation level, we move from Evaluation-based deployment (which is slow, requires source access, and may be prone to evaluation-time impurities potentially) to a Plan-based deployment . We now treat the build graph, via the derivations, as a portable artifact that can be relocated into any realm, regardless of whether that realm has the source code, the right version of Nixpkgs, or in the case of Guix, even speaks the same front-end language.

0 views
Farid Zakaria 2 months ago

The Guix Nix Abomination: Leveraging Guix derivations in Nix

Nix and Guix look like rival ecosystems, but under the hood they’re the same “Input Output Machine”. Need proof? 🕵 How about we build a Guix derivation with Nix. First let’s create a super basic derivation in Guix: Hello world . We then ask Nix to build it. 🪄 We ask to use as the Nix store and have it write its state, database and log files in alternate directories, so it does not collide or mess with Guix. Note It’s slightly more complicated. Nix happens to check its SQLite database for the derivation, so we need to register it first. The version of Guix (v1.5.0) I’m using leverages a user that runs inside a private mount namespace where is writable, but everyone else (including me) sees it as read-only. The creates a new private mount namespace so I can mount it as read-write and run the Nix command against it. We just built a Guix derivation using Nix. 🔥 How is that possible? Both take a language frontend, Nix or Guile (Scheme), that compiles to a derivation (recipe) and pass that onto a builder (daemon) that executes it to produce an output. What makes them both special is they both promise the same thing: hermetic builds . Everything needed to build the output is declared in the recipe: sources, environment variables, dependencies, etc. “Under Nix, a build process will only find resources that have been declared explicitly as dependencies. There’s no way it can build until everything it needs has been correctly declared. If it builds, you will know you’ve provided a complete declaration.” – Nix OS Website Guix, specifically the daemon, was forked from Nix early on, and as a result the two are very similar; they both share the same derivation format, ATerm , for instance. Guix is based on the Nix package manager – Guix Website That’s why our earlier example of building the Guix derivation with Nix was possible without much translation. What if we could leverage an existing recipe from Guix in Nix in its traditional ? If we could convert from one recipe file to the other, we could use the existing recipes from Guix in Nix and vice versa. Turns out this is far more feasible than you would think, because Guix is Nix or at least a superset of it. I, with the help of Claude, built a tool to do just that: guix-transfer 🤯. guix-transfer is a CLI tool for performing bottom-up translation of GNU Guix derivations into Nix. Confused? Let us see it in action: Note When you unpack a tarball, tar restores each file’s original permissions, including setuid/setgid bits. Nix’s sandbox installs a seccomp filter that blocks any call that sets these bits, returning “Operation not permitted”. Guix’s early bootstrap uses a Scheme-based (gash-utils) that treats this error as fatal, unlike GNU tar which silently skips it. The fix is , which disables the filter. If it’s not clear what we just did: we took a Guix derivation and all of its dependencies (down to the bootstrap seeds), translated it to a Nix derivation, and built it with Nix. 😲 What is this abomination and how was this possible!? It’s important to revisit what a derivation is, and how it’s used in Nix and Guix to better understand how this is possible. Let’s look at the same basic derivation from earlier, Hello World . You might want to check out my other post on Nix derivations by hand if this interests you 🤓. When we evaluate (nix-instantiate) this derivation, we get a path to a file that contains the derivation in the ATerm format: If we look at the contents of the file, we can see the ATerm representation of the derivation: This has all the information we need to build the output by the builder. At this point, it’s really not Nix specific anymore. The same applies for the Guix derivations. The derivations do not “know” whether they came from Scheme or Nix. It’s a recipe. The insight then is if we rewrite the store paths from to , and swap some builtins (i.e. for ), we can get to build it identically . 💡 The only difference in more complex derivations is that they have dependencies, which are also derivations, and the builder references them so it forms a graph of derivations, each built by the builder in topological order. The leaves of this tree for any non-trivial derivation are the bootstrap seeds: , , , etc. Guix is famous for bootstrapping itself from a 357-byte binary as source [ ref ]. Since at no point do the bootstrap seeds depend on being the prefix, the translated chain builds identically under Nix. walks a Guix graph in post-order and for each derivation: Guix’s is replaced with Nix’s . Same idea, different name. Source files are added to the Nix store, with embedded paths rewritten to their equivalents. Every reference: input drvs, builder path, args, env vars are rewritten to the mapped path. Output paths are blanked as Nix recomputes them via . The result is serialised as JSON and registered with . That’s it. No Nix expressions are generated. No . No mapping of Guix packages to nixpkgs equivalents. The Guix derivation graph is translated faithfully , and builds it. Note Interestingly, takes exactly one URL and cannot fall back. Guix derivations carry lists of mirrors, many of which are flaky or dead. Similar to Nix, Guix operates a content-addressed mirror at that serves any source its CI has ever seen. We leverage this for the instead of the original source URL. Now that we have a way to slurp Guix packages into Nix, we can start to do some diabolical combinations by combining native Nix and Guix packages together! We can take our package we built in Nix and leverage it in a Nix derivation. Nix automatically scans your derivations for anything prefixed with and tracks it as an input dependency. This is similar to how store paths are interpolated when you do something like . If writing the paths raw in the Nix expression is a little too raw for you, we can build something more ergonic pretty easily as well. has an mode that instead will emit the Nix expression for the translated . Let’s look at a slightly more complex example that uses Guix’s to build a derivation with dependencies: We can now convert this to a Nix expression with . We realise the derivation with or we can the Nix expression. Please notice that both produce the exact same hash : . We can now use this Guix derivation like any normal Nix expression, such as the ones you might encounter in Nixpkgs. That means we could even build a that is all of Guix packages available for use. My mind is blown. 🤯 Nixpkgs is known as the world’s largest package repository, and now we have made a way for it suddenly to become even larger by borrowing any derivation from Guix! The real power behind Nix are the derivations and that they are hermetic, declaring any dependency needed. We’ve seen that we can transfer these recipes to any store-based system that has similar qualities and preserve the reproducibility. Guix’s is replaced with Nix’s . Same idea, different name. Source files are added to the Nix store, with embedded paths rewritten to their equivalents. Every reference: input drvs, builder path, args, env vars are rewritten to the mapped path. Output paths are blanked as Nix recomputes them via . The result is serialised as JSON and registered with .

0 views
Farid Zakaria 2 months ago

Every byte matters

I have spent a large portion of my career working in Java. In that time, you get used to huge classes. New functionality? Just add a new method and field to the class. The cost of each new field is rarely considered. Performance is often considered from a classic computer science perspective by considering asymptotic analysis of the algorithms and data structures in-use. Turns out that even within a growth scale for your algorithm, such as a simple for-loop , time can vary dramatically if we have a little deeper understanding of the underlying hardware. First, let’s understand our current machine. Let’s take a peek at our cache line and page sizes. The instances number is a reflection of how the caches are shared amongst CPUs. If I had 10 CPUs, each one has their own cache, whereas two of them would share an cache. Our cache line size is 64 bytes . When you read a single byte from memory, the hardware will fill the surrounding 64 bytes into the cache line. The idea being that data is often temporal and spatially located, meaning data is often accessed near each other and close in time to each other. We can reference Jeff Dean’s famous “Latency numbers every programmer should know” , however a quick recap with the values from our particular machine is the following: The sizes for each cache, is the number returned by divided by the number of cores or instances; i.e. 352 KiB ÷ 10 instances = ~35 KiB. We then determine the number of cache lines by dividing this number by 64; i.e. 35 KiB ÷ 64 bytes = 560 cache lines. How does this all matter ? 🤔 Let’s consider an example where we want to iterate over a single struct and pull out the to filter them. We create our struct, and in this particular example we need 64 bytes to represent a single Monster. If we had an array of Monsters and we iterate over them, the cache line would fill up like so. Each cache line would fill with a single monster, and we would fetch only the byte. This is often referred to as “Array of Structs”. If we instead normalize the data such that each field is in it’s own list, we can pack the cache lines much tighter. This type of layout is referred to as “Struct of Arrays”. How much of an impact can this have? We can observe up to 30x improvements when the Monster struct is 1KiB 🤯 The delta is less observable when the struct is small because multiple Monster structs can still be fetched within a single cache-line. This data access is incredibly hot though. Your CPU pre-fetcher knows it’s going sequentially and fetches the next cache line before you need it. You never actually have to wait for the memory to be fetched. What about random access patterns? Not all access patterns are sequential. Hash maps, trees, graph traversal, and pointer-heavy data structures jump to unpredictable locations. The CPU can’t prefetch what it can’t predict. With random access, the CPU needs the entire array to be present in the cache in order to avoid stalls due to memory lookup. This means the total size of your collection determines your performance tier. Doubling the struct from 64B to 128B doubles the working set for the same number of monsters, pushing the data into slower cache levels. At just 512 monsters, a 64B struct fits in L1d at ~3 ns — but a 128B struct has already spilled to L2 at ~11 ns. We can observe this with a pointer-chasing benchmark. We allocate N monster-sized nodes, wire them into a random order, and chase pointers. Each hop lands at an unpredictable address, defeating the CPU’s prefetcher entirely. Rather than graph it logarithmically, which I find sometimes is easy to miss, I have included a zoomed in graph. We can see that all struct sizes hit the same staircase like pattern as they go through the various cache levels however the larger struct sizes are shifted left , meaning they hit the increase earlier. This means for random access patterns, if you can keep tight control on your total working set size, you can drastically affect the time. Knowing your struct and working set size can make a substantial difference.

0 views
Farid Zakaria 2 months ago

AI is a Boon for the Anal-Retentive

I guess it’s my turn to write an “AI article”. 🫠 It’s Saturday evening and I’m late into my 3rd project, one where I would never have gotten so far without the help of recent advancements in AI, all thanks to LLMs. The realization that I’ve been able to accomplish personal projects that evaded me for so long was really remarkable and I feel fortunate…at least for now. I can see the knobs slowly dialing back, the amount of tokens we are given pro bono is quickly diminishing. A typical project, B.O. (Before Opus 4.6), was one where I think of an idea that inevitably needs a frontend component. I then spend 3-7 days researching the latest on frontend build patterns, frameworks and trends. I’m then mired in choice and complexity only to lose focus, or lose interest in bit-twiddling CSS (something I never quite truly learned). My latest project, Zephyr , is my attempt to build a weather wind station powered by battery & solar that can send me updates via cellular network. The project involves: writing firmware via Rust to an ESP32 board, sending commands to the modem to initiate the HTTPS requests, connecting the anemometer and wind vane to the device, working with a breadboard (I can do hardware now too!) and writing a frontend website to display the results. To say this project is outside of my wheelhouse is an understatement . However, I have been able to get surprisingly far asking for advice and guidance, and leaning on the LLM to bootstrap some code. I have had to be incredibly thoughtful in guiding it to relevant schematics, examples and manuals since writing the firmware for the board was incredibly tricky with all the various knobs that can be tuned. The project prior to that was one dedicated to recording surfing entries: http://surfing.exe.xyz/ . The project prior to that was https://checkthisdealforme.com/ , a small website where you can quickly appraise items you either want to buy or sell. You might notice a theme that all these projects leverage https://exe.dev/ . It’s a platform I’ve enjoyed that has similarly taken all the infrastructure molasses out when working on small projects. All these projects would never have made it this far. I’m far too anal-retentive. That quality is part of my personality. It has often been a strength when dealing with the mire of complexity at $DAYJOB$ but has been a burden for things whose complexity I am happy to relinquish. Thank you, large language models, for freeing me of my anal-retentiveness when necessary. While others use the term “slop” as a form of insult, here it’s been a boon and a welcomed one.

0 views
Farid Zakaria 2 months ago

Leaving performance on the table

I have been working with LLVM at , and I have gotten to become familiar with the benefits of optimizing your workloads. I tend to think of optimizing my binaries as thinking about whether I have attached to my compiler flags; maybe if I’m particularly advanced that day I’ll sprinkle in some (link time optimziation) and call it a day. Turns out though that’s leaving lots of performance on the table. Compilers work under the assumption that every branch is is equally taken, unless you are hints like ( ref ). If we can feed the compilers more information about the likely path that our workloads often take, then they can produce much more performant code. There are two primary ways to optimize a binary: instrumented or statistical. When we instrument our binary, we run our workload with an instrumented binary and capture the exact paths that are executed. We will then optimize the binary perfectly tuned to that workload. If our workloads however are varied, we can collect profiles via over a length of time and create an optimized binary based on the statistical occurence of call graphs. Both approaches have their benefits however let’s start with the instrumented variant first, as it’s a little easier to follow and understand. Let’s look at a very simple benchmark. We will calculate fibonocci using SQL in sqlite3 . This is an ideal workload because it’s purely CPU-bound and ripe for optimizing. We will compile from source by downloading it. We can compile a “traditional” optimized binary that merely has and also a version that has LTO enabled since I was also keen to see how much LTO itself adds. Ok, so it looks like our program takes roughly 14-15 seconds to run. Sounds ok? How much better can we do…. 🤔 Next, we compile our program again but we instrument the binary , which effectively injects counters into the program to count invocations of functions. We get very accurate counts of our calls but the binary itself now runs much slower, which can be a problem if your workload was already very slow. Luckily for us, we are in a time domain (~15 seconds), where that is ok. After we have our instrumented binary, we run our workload again to generate the profile data and rebuild the binary with that data. The last step will be to optimize with BOLT, which is a post-link optimizer, which requires us to keep relocations so I’ve also added . When we run our workload with the final optimized binary, we see massive improvement already! 🤯 We’ve cut our workload time down to ~10 seconds which is a nearly a 1.5x improvement. Now let’s optimize the final binary with LLVM’s BOLT . BOLT is a post-link optimizer designed for “large applications”. What this means, is that it largely works by shuffling code around the binary to keep code-paths that have high temporal locality near each other (spatial locality). This can have positive impact on performance due to the instruction cache for instance. Looks like it was a little faster but not much. That makes sense since itself is a pretty small binary (~6MB), but nontheless was good to run through. Running a more thorough benchmark with we can get a final tally of our results. Looks like the I got from the Fedora ecosystem was the slowest . When all the optimizations were applied I was able to get a maximum of 1.38x faster than what was available. These optimizations would be even more dramatic for code-bases that are a sprawl and can heavily vary. Don’t worry also about getting the profile perfectly tuned to your workloads. I have a coworker who often cites that even poor profiles are still much better than no profile at all.

0 views
Farid Zakaria 4 months ago

Does anyone actually use the large code-model?

I have been focused lately on trying to resolve relocation overflows when compiling large binaries in the small & medium code-models. Often when talking to others about the problem, they are quick to offer the idea of using the large code-model. Despite the performance downsides of using the large code-model from the instructions generated, it’s true that its intent was to support arbitrarily large binaries. However does anyone actually use it? Turns out that large binaries do not only affect the instructions generated in the section but may also have effects on other sections within the ELF file such as (exception handling information), (optimized binary search table for ), and even . Let’s take and as an example. They specifically allow various encodings for the data within them ( or for 4 bytes and 8 bytes respectively) irrespective of the code-model used. However, it looks like the userland has terrible support for it! If we look at the format, we can see how these encodings are applied in practice. The entries in this column are the ones that actually resolve to specific DWARF exception header encoding formats (like , , , etc.) depending on the values provided in the preceding fields. format [ ref ]: Note: The values for and dictate their byte size and format. For example, if is set to , the field will be processed as an (signed 4-byte) value. Up until very recently ( pull#179089 ), LLVM’s linker would crash if it tried to link exception data ( ) beyond 2GiB. This section is always generated to help stack searching algorithms avoid linear search. Once we fix that though, it looks like ( gcc-patch@ ) and ( pull#964 ) explicitly either crash on or avoid the binary search table completely reverting back to linear search. How devasting is linear search here? If you have a lot of exceptions, which you theoretically might for the large code-model, I had benchmarks that started at ~13s improve to ~18ms for a ~700x speedup . Other fun failure modes that exist: Note: Don’t let confuse you, it’s actually 32bit: It seems like the large code-model “exists” but no one is using it for it’s intended purpose which was to build large binaries. I am working to make massive binaries possible without the large code-model while retaining much of the performance characteristics of the small code-model. You can read more about it in x86-64-abi google-group where I have also posted an RFC.

0 views
Farid Zakaria 5 months ago

Nix is a lie, and that’s ok

When Eelco Dolstra , father of Nix, descended from the mountain tops and enlightened us all, one of the main commandments for Nix was to eschew all uses of the Filesystem Hierarchy Standard (FHS) . The FHS is the “find libraries and files by convention” dogma Nix abandons in the pursuit of purity. What if I told you that was a lie ? 😑 Nix was explicitly designed to eliminate standard FHS paths (like or ) to guarantee reproducibility. However, graphics drivers represent a hard boundary between user-space and kernel-space. The user-space library ( ) must match the host OS’s kernel module and the physical GPU. Nearly all derivations do not bundle with them because they have no way of predicting the hardware or host kernel the binary will run on. What about NixOS? Surely, we know what kernel and drivers we have there!? 🤔 Well, if we modified every derivation to include the correct it would cause massive rebuilds for every user and make the NixOS cache effectively useless. To solve this, NixOS & Home Manager introduce an intentional impurity, a global path at where derivations expect to find . We’ve just re-introduced a convention path à la FHS. 🫠 Unfortunately, that leaves users who use Nix on other Linux distributions in a bad state which is documented in issue#9415 , that has been opened since 2015. If you tried to install and run any Nix application that requires graphics, you’ll be hit with the exact error message Nix was designed to thwart: There are a couple of workarounds for those of us who use Nix on alternate distributions: For those of us though who cling to the beautiful purity of Nix however it feels like a sad but ultimately necessary trade-off. Thou shall not use FHS, unless you really need to. nixGL , a runtime script that injects the library via manually hacking creating your own and symlinking it with the drivers from

0 views