Fil-C on Nix

Filnix wiki

Releases

BIND 9.20.30 · Fil-C 0.686 · October 2026

Fil‑BIND: BIND 9 in memory‑safe C

BIND 9.20.30 — and OpenSSL, libuv, liburcu, nghttp2, libxml2 and the C library underneath it — compiled with Fil-C. An out-of-bounds access or a use after free stops named instead of corrupting its memory: the likely worst case becomes a crash, not a takeover. It is a research build, and this page is honest about the trade: about five times the CPU per query.

The idea in 40 seconds. Made with Manim (source); the page below has the details.

Try it

Docker — the image is laid out like ISC's internetsystemsconsortium/bind9: configuration in /etc/bind, working directory /var/cache/bind, named as the bind user, same entrypoint. A configuration that works there should work here:

docker run --rm -p 53:53/udp -p 53:53/tcp \
  -v $PWD/named.conf:/etc/bind/named.conf ghcr.io/mbrock/filnix-bind:9.20.30

Tarball — bind-9.20.30-filc-x86_64-linux.tar.xz from the releases runs on any x86-64 Linux, glibc or musl, without Nix: it carries its own runtime and libraries.

Nix — pkgsFilc is filnix's Fil-C package set, and BIND in it is one flake attribute:

nix build github:mbrock/filnix#pkgsFilc.bind   # the package itself
nix build github:mbrock/filnix#bind-dist       # the tarball above
nix build github:mbrock/filnix#bind-docker     # the image above

On NixOS

filnix's NixOS is a whole system whose packages are Fil-C builds, so a BIND server there is simply the NixOS BIND module: services.bind takes pkgs.bind, and pkgs is the Fil-C package set. Your zones and options stay exactly as they are:

# flake.nix
inputs.filnix.url = "github:mbrock/filnix";
nixosConfigurations.ns1 = filnix.lib.nixosSystem { modules = [ ./configuration.nix ]; };

# configuration.nix
services.bind = {
  enable = true;
  zones = { # …exactly as usual };
};

systemd, PAM, OpenSSH and the rest of the machine are Fil-C too; only the kernel, the bootloader, Nix and a short list of exceptions stay native, and filc-ps on the machine checks that nothing else runs native. The Fil-C packages come from filnix's own pinned Nixpkgs and its binary cache, so a rebuild mostly downloads. Nixpkgs' own BIND VM test runs on this system, followed by the same check. To run only named as Fil-C on an ordinary NixOS, set services.bind.package = filnix.legacyPackages.x86_64-linux.pkgsFilc.bind;; the NixOS notes say what that leaves out.

Why now: 37 advisories, 6 of them Fil-C's business

ISC has published 37 BIND 9.20 advisories since March 2026, at a rate it attributes to vulnerability hunting with language models. The 21 October release, 9.20.30, fixes CVE-2026-94300: replayed GSS-TSIG messages corrupt memory, with “potential remote code execution”.

Most of the 37 are not memory errors, and Fil-C does nothing about them — worth being precise about, because it is the whole scope of the claim:

12 CPU/memory exhaustion — unchanged11 assertion failures — named already stops itself8 logic errors, e.g. cache poisoning — unchanged6 memory-safety bugs — the kind Fil-C catches
BIND 9.20 advisories since March 2026, by kind. Fil-BIND is about the red slice — and the next one like it.
AdvisoryFixed inBugWhat we expect under Fil-C
CVE-2026-943009.20.30Replayed GSS-TSIG messages corrupt memory; potential RCEThe corrupting access stops the process
CVE-2026-35939.20.23Heap use after free in DNS-over-HTTPSThe freed object cannot be read or written
CVE-2026-59479.20.23SIG(0) validation reads a discarded messageStops every time, instead of sometimes reading reused memory
CVE-2026-196629.20.29Use after free of a cached NOQNAME proofAlready a crash; now a defined one
CVE-2026-196669.20.29Use after free in the DNS64 pathAlready a crash; now a defined one
CVE-2026-35919.20.21Stack use after return lets an ACL mismatchUnclear: Fil-C keeps escaped locals alive, so the stale read may succeed

These are expectations from how Fil-C works, not reproductions: we have not replayed these exploits against Fil-BIND, and the build is 9.20.30, which has all of them fixed anyway. The point is the class of bug. When the next one is found — in BIND, or in OpenSSL, libuv or nghttp2 underneath it — the likely worst case is a crash: a denial of service rather than a takeover.

How Fil-C stops these

Fil-C gives every pointer an invisible capability recording the object it points into. Each load and store is checked against it, so a write past the end of a buffer stops the program instead of landing in the neighbouring object. free marks the object freed, so a later access through any old pointer also stops, and a garbage collector keeps the memory from being reused while such pointers exist.

One BIND-specific adjustment matters: BIND recycles some objects itself, through memory-pool free lists of message names, rdatasets and network sockets. A recycled object stays valid, so a use after returning it to the pool would go unseen — the exact blind spot the pool creates for any checker. Fil-BIND turns those free lists off, as BIND's own AddressSanitizer builds do.

What it took

BIND itself compiled unchanged. Getting it to run needed three things, plus the free lists above:

BIND's unit tests then pass, except three that replace a library function by defining it in the test program, which Fil-C's linking does not allow (findings). A signed zone served by one named and validated by another, AXFR, TSIG-signed updates and rndc all work, in plain Debian and Alpine containers as root and as an unprivileged user.

What it costs

Fil-C's checks and garbage collector cost a lot in a server that does little but move small packets through pointer-heavy data structures. Measured with dnsperf against a 20,000-name zone, on an 8-core cloud VM with the load generator on the same machine, Nixpkgs' native BIND 9.20.29 against Fil-BIND; each number is the average of two 15-second runs:

nativeFil-BIND
Authoritative, 1 thread167,000 qps31,500 qps5.3× slower
Cached resolver answers, 1 thread160,000 qps28,600 qps5.6× slower
Authoritative, 4 threads409,000 qps64,400 qps6.4× slower
Cached resolver answers, 4 threads382,000 qps74,000 qps5.2× slower
Resident memory, resolver39 MB167 MB≈4×

Roughly five to six times the CPU per query, and four times the memory. Thirty thousand queries a second on one thread is far below what BIND can do, but well above what many internal, small-office or legacy servers ever see — which is the niche this is for.

The first build was about ten times slower. Profiling showed the garbage collector's marking taking half of named's CPU: under Fil-C, a local variable whose address escapes becomes a zeroed heap allocation, and each query made about 60 KB of them — mostly QP-trie lookup chains and iterators, search contexts and client state. Building those structs in place, and allocating them only when a caller needs them, cut that to about 15 KB and roughly doubled throughput (patch). Much of what remains is in the Fil-C runtime itself: copying and zeroing small structs that hold pointers, and setting up their capability metadata.

A research artifact. This is not an ISC build and comes with no support or warranty. Fil-C turns memory corruption into a crash; it does not stop assertion failures, resource exhaustion or cache poisoning, so keep following ISC's advisories and upgrading. If you run an old BIND setup where a crash is better than a compromise and five times the CPU is affordable, it may be worth evaluating. The build recipe is in mbrock/filnix.