NixOS: Quick fix for PHP CVE-2026-109618 and CVE-2026-109619

21 Oct 2026

PHP disclosed two CVSS 9.2 unauthenticated RCE vulnerabilities on 21 October 2026, with exploitation expected within minutes. Patched releases like 8.3.36, 8.4.27 were available immediately, but nixpkgs takes days to catch up: PR, review, Hydra rebuild of PHP and its reverse dependencies, channel advance. Our flake pins a nixpkgs commit, so waiting also means bumping the pin and rebuilding everything else.

An overlay swaps only the PHP source.

{ lib, ... }:
let
  patched = {
    php83 = { version = "8.3.36"; hash = "sha256-RvS0XjaI7SsYGQFae1MsFbUcwFW907E/0A3f7r7tzV8="; };
    php84 = { version = "8.4.27"; hash = "sha256-slMKh3xyL0sIyeF0owywi6wrfhkD7m6eMV8KqfX4LCQ="; };
  };

  mkPhpOverride = name: attrs: _final: prev: {
    ${name} = prev.${name}.overrideAttrs (_: {
      inherit (attrs) version;
      src = mkPatchedSrc prev attrs;
    });
  };
in
{
  nixpkgs.overlays = lib.mkAfter (map (name: mkPhpOverride name patched.${name}) (builtins.attrNames patched));
}

overrideAttrs on the buildEnv wrapper propagates to the unwrapped derivation and rebuilds every enabled extension, so one override covers FPM pools, CLI, Composer and phpXXPackages. lib.mkAfter keeps it last so it wins over other overrides.

The sandbox problem

The build failed on three new tests in ext/filter:

FAIL filter_input() doesn't reach hard limit [tests/oom_filter_input.phpt]
FAIL filter_input_array() doesn't reach hard limit [tests/oom_filter_input_array.phpt]
FAIL filter.default=encoded doesn't reach hard limit while parsing POST data [tests/oom_filter_default_encoded.phpt]

They spawn php-cgi with memory_limit=12M and assert on the fatal-error output, which doesn't reproduce in the Nix build sandbox.

doCheck = false on the extension isn't reachable: extensions live in a packageOverrides scope the buildEnv wrapper doesn't expose, and overriding the extensions function directly causes infinite recursion. So we patched the source instead:

mkPatchedSrc = prev: { version, hash }:
  let
    src = prev.fetchurl {
      url = "https://www.php.net/distributions/php-${version}.tar.bz2";
      inherit hash;
    };
  in
  prev.runCommand "php-${version}-src" { } ''
    mkdir -p "$out"
    tar -xf ${src} -C "$out" --strip-components=1
    rm -f "$out"/ext/filter/tests/oom_filter_input.phpt \
          "$out"/ext/filter/tests/oom_filter_input_array.phpt \
          "$out"/ext/filter/tests/oom_filter_default_encoded.phpt
  '';

Interpreter and extensions share the same src, so removing three files once covers all of them. The interpreter still builds from the patched upstream source.

Takeaway

An overlay is the right tool when a fix is minutes old and the package set is days behind. A few lines, reversible, gone once nixpkgs catches up. The only friction was the test suite — a sandbox limitation, not a problem with the patch.

Add new comment

The content of this field is kept private and will not be shown publicly.

Restricted HTML

  • Allowed HTML tags: <a href hreflang target> <em> <strong> <cite> <blockquote cite> <pre> <ul type> <ol start type> <li> <dl> <dt> <dd> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.

Get a quote in 24 hours

Wether a huge commerce system, or a small business website, we will quote the project within 24h of you pressing the following button: Get quote