Fil-C on Nix

Filnix wiki

Releases

PHP 8.5.12 by default · x86-64 Linux · November 2026

PHP on Fil‑C: memory‑safe PHP

The existing PHP interpreter, its native extensions and the C/C++ libraries underneath them, compiled with Fil-C. Your PHP code stays the same. A memory error in PHP or an extension stops the worker instead of corrupting its memory.

This is a port, not a rewrite of PHP in another language. Targeted changes preserve the PHP ecosystem—including extensions such as Redis, MongoDB, Imagick and their native libraries. The larger piece of new work is a backend that lets OPcache generate checked code through Fil-C's LLVM JIT.

Can it serve a real website?

A small internal wiki or blog is a plausible evaluation target. WordPress, MediaWiki and DokuWiki work with the interpreter and with the checked LLVM JIT. But “it runs” is not enough: page latency, throughput under concurrent traffic, cold workers and memory use all matter.

We measure complete HTTP responses through nginx → PHP-FPM → SQLite, without a web-server full-page cache. An ordinary article view is measured separately from a mix of views, search, history, feeds and API requests. These are server response times over loopback, not browser load times; Internet latency, TLS and fetching assets are extra.

Site / runtimeArticle median / p95Mixed req/s
MediaWiki — native57 / 80 ms47.4
MediaWiki — Fil-C interpreter184 / 197 ms11.1
MediaWiki — Fil-C LLVM JIT209 / 229 ms8.4
WordPress — native46 / 48 ms47.1
WordPress — Fil-C interpreter277 / 286 ms7.2
WordPress — Fil-C LLVM JIT265 / 274 ms7.1

Three-round medians · Chapel, November 5, 2026 · Core i7-10700K · two FPM workers · 120 requests/load/round. Article: one client; mixed throughput: four. Both Fil-C builds are staging PHP 8.5.12, not the released-toolchain default. Initial warm-up excluded; occasional later compilation included. Builds, mixed-load p95 and limits · Reproduce

Hundreds of milliseconds, not seconds, for these warmed articles. That makes an internal site worth evaluating, but the two-worker pool's throughput is modest and concurrent requests queue. LLVM did not consistently improve CMS performance. Keep the interpreter as a baseline; don't assume a JIT is necessary.

The older benchmark's “1.3 s for MediaWiki” was a sum across twelve different requests, not a wiki page taking 1.3 seconds. Detailed comparisons now live in the port notes.

Warm workers before sending them traffic. In this run, two-worker pools took roughly 8–10 minutes to complete their first CMS request mix with LLVM, versus about 4 seconds interpreted. LLVM workers held about 1 GiB or more RSS each, versus 0.5–0.6 GiB interpreted; RSS includes shared pages. The requests that trigger compilation wait. A warm benchmark does not measure what the first visitor after a restart experiences.

For sizing, replay your own traffic and watch p95 latency at the expected concurrency—not just requests per second at saturation. The fixtures are small SQLite-backed sites, not a large wiki or a plugin-heavy WordPress with remote MySQL. Results on bare metal are not a promise about a small VM.

The LLVM JIT is the path forward

php85-jit is the experimental PHP 8.5 build to evaluate if you want a JIT. OPcache still identifies hot PHP code; the backend translates its IR into LLVM IR and calls Fil-C's in-process compiler. KillUB and FilPizlonator check the generated code just as they check compiled C. No external compiler process runs, and side traces can link to each other.

nix build github:mbrock/filnix#php85-jit
result/bin/php -d opcache.enable_cli=1 -d opcache.jit=tracing \
  -d opcache.jit_buffer_size=64M script.php

This stays on staging. It uses unreleased upstream Fil-C work plus our additions, pinned in lib/filc-staging.json. It cannot move to the released toolchain until a Fil-C release provides the required LLVM API. Making PHP 8.5 the default does not make this experimental toolchain the default.

LLVM is the JIT to try, not a guarantee of faster CMS pages. The earlier 4.7× speedup on CPU-bound Zend/bench.php does not predict website latency. Much of a CMS's work is in checked strings, arrays, PCRE, allocation and the database, not PHP opcodes. Use opcache.jit=tracing; compiling whole functions is much more expensive.

The C-emitting backend is an earlier experiment. Printing C, invoking Fil-C clang and loading the result proved the approach, but has been superseded as the starting point by the LLVM API backend. It remains available with PHP_JIT_BACKEND=c; php-jit still names the historical 8.4 build. Its implementation and older comparisons belong in the port notes, not the main deployment decision.

Every FPM worker normally compiles its own code. An opt-in shared LLVM object cache reduces some duplication, but is off by default and has a documented security caveat. We do not recommend switching it on just to shorten startup.

Try PHP 8.5

The ordinary PHP default uses the released Fil-C toolchain, OPcache and the interpreter. The JIT above is a separate opt-in build.

nix build github:mbrock/filnix#pkgsFilc.php   # PHP 8.5, default extensions
nix build github:mbrock/filnix#php-dist      # relocatable tarball, no Nix at runtime
nix build github:mbrock/filnix#php-docker    # OCI image

The tarball carries php, php-fpm, php-cgi, phpdbg, the runtime and its libraries. Configure a binary cache before building. Published releases retain their original versions; check the release assets rather than assuming an old :latest image has changed.

nix build github:mbrock/filnix#php-docker && docker load < result
docker run --rm -p 9000:9000 -v "$PWD:/var/www/html" \
  filnix-php:8.5.12 php-fpm

Port 9000 is FastCGI, not HTTP: put nginx or Caddy in front, as with php:fpm. NixOS users can switch existing pools with filc.phpfpm.enable = true; the module guide covers supported web applications and per-pool overrides. withExtensions works as in Nixpkgs; see the extension list. Binary extensions must also be Fil-C builds. PHP 8.5 currently lacks the pinned igbinary, memcache and Phalcon versions, which do not build on native 8.5 either.

What protection do you get?

Each PHP value gets its own bounds. The default tracked system allocator gives each value a separate allocation. Zend's allocator groups values into 2 MiB chunks, hiding overflows and use after free inside a chunk from Fil-C. Keep the default: USE_ZEND_ALLOC=1 weakens that protection, and USE_TRACKED_ALLOC=0 gives up memory_limit.

It stops a real exploit chain. The adapted StealMyBitcoin exploit succeeds against ordinary vulnerable PHP 8.4.25. Against the same vulnerable PHP compiled with Fil-C, a capability trap stops the first corrupted read before the heap leak, so the later command-execution stages cannot begin. The reproduction explains what was tested and which routes were not.

A trap is still a crash. FPM can replace the worker, but an attacker may still cause denial of service. Fil-C does not stop SQL injection, authentication errors, object-injection gadgets or malicious PHP code. Keep PHP patched. This is an experimental distribution, not a sandbox or a supported php.net build.

OPcache is per-worker. Workers inherit what the master populated before forking but cache privately afterward; opcache_reset() only resets the worker that receives it. Account for that in deployments and cache invalidation.

Other versions and deeper notes

pkgsFilc.php84 remains PHP 8.4.27. pkgsFilc.php74 and php74-dist are end-of-life migration aids, not supported PHP. The explicitly named php84_25 outputs are deliberately vulnerable and only for exploit reproductions.

The deployment guide covers performance, build choices and operations. The port notes retain the allocator and VM changes, both JIT implementations, detailed benchmark tables, exploit evidence and version-specific test results.