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.
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:
named already stops itself8 logic errors, e.g. cache poisoning — unchanged6 memory-safety bugs — the kind Fil-C catches| Advisory | Fixed in | Bug | What we expect under Fil-C |
|---|---|---|---|
| CVE-2026-94300 | 9.20.30 | Replayed GSS-TSIG messages corrupt memory; potential RCE | The corrupting access stops the process |
| CVE-2026-3593 | 9.20.23 | Heap use after free in DNS-over-HTTPS | The freed object cannot be read or written |
| CVE-2026-5947 | 9.20.23 | SIG(0) validation reads a discarded message | Stops every time, instead of sometimes reading reused memory |
| CVE-2026-19662 | 9.20.29 | Use after free of a cached NOQNAME proof | Already a crash; now a defined one |
| CVE-2026-19666 | 9.20.29 | Use after free in the DNS64 path | Already a crash; now a defined one |
| CVE-2026-3591 | 9.20.21 | Stack use after return lets an ACL mismatch | Unclear: 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:
- liburcu's lock-free hash table, which BIND uses for its dispatch, bad-cache and zone tables, trapped on every table: a
bsrinline-assembly helper, taggednextpointers whose flag arithmetic dropped their capabilities, and a flag set by an integer atomic that later pointer loads did not see (patch). - BIND's qp-trie kept each leaf pointer as two 32-bit halves of a 12-byte node, which loses the capability. Under Fil-C the node holds a real pointer and grows to 16 bytes (patch).
- The memory pools, above (patch).
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:
| native | Fil-BIND | ||
|---|---|---|---|
| Authoritative, 1 thread | 167,000 qps | 31,500 qps | 5.3× slower |
| Cached resolver answers, 1 thread | 160,000 qps | 28,600 qps | 5.6× slower |
| Authoritative, 4 threads | 409,000 qps | 64,400 qps | 6.4× slower |
| Cached resolver answers, 4 threads | 382,000 qps | 74,000 qps | 5.2× slower |
| Resident memory, resolver | 39 MB | 167 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.