Fil-BIND: BIND 9 in memory‑safe C
BIND 9.20.30 and every library it uses, compiled with Fil-C. An out-of-bounds access or a use after free stops named instead of corrupting its memory. It is a research build: interesting to evaluate, not a promise.
Try it
The container image is laid out like ISC's internetsystemsconsortium/bind9: configuration in /etc/bind, the working directory /var/cache/bind, and named running as the bind user with the 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
The release also has a relocatable tarball, bind-9.20.30-filc-x86_64-linux.tar.xz, that runs on any x86-64 Linux, glibc or musl, without Nix: it carries its own runtime and libraries. With Nix, nix build github:mbrock/filnix#bind-dist, or #legacyPackages.x86_64-linux.pkgsFilc.bind for the package itself.
Why now
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. Eleven are assertion failures, where named already stops itself, twelve are CPU or memory exhaustion, and eight are logic errors such as cache poisoning. Six are memory-safety bugs, the kind Fil-C is built to catch:
| 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 that records 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. OpenSSL, libuv, liburcu, nghttp2, libxml2 and the C library are compiled the same way, so the code underneath named is checked too.
BIND recycles some objects itself: its memory pools kept free lists of message names, rdatasets and network sockets, and a recycled object stays valid, so a use after returning it would go unseen. Fil-BIND turns those free lists off, as BIND's own AddressSanitizer builds do.
What it took
BIND itself compiled unchanged. Running it needed three things:
- 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. We measured with dnsperf against a 20,000-name zone, on an 8-core cloud VM with the load generator on the same machine, comparing Nixpkgs' native BIND 9.20.29 with Fil-BIND:
| native | Fil-BIND | ||
|---|---|---|---|
| Authoritative, 1 thread | 161,000 qps | 16,700 qps | 9.6× slower |
| Cached resolver answers, 1 thread | 166,000 qps | 17,600 qps | 9.4× slower |
| Authoritative, 4 threads | 416,000 qps | 31,800 qps | 13× slower |
| Cached resolver answers, 4 threads | 337,000 qps | 36,600 qps | 9.2× slower |
| Resident memory, resolver | 41 MB | 162 MB | ≈4× |
So roughly ten times the CPU per query, and four times the memory. Seventeen thousand queries a second per core is far below what BIND can do, but well above what many internal, small-office or legacy servers ever see. We have not tuned anything yet.
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 ten times the CPU is affordable, it may be worth evaluating. The build recipe is in mbrock/filnix.