<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Stefano Franco</title>
    <description>The latest articles on DEV Community by Stefano Franco (@sphranko).</description>
    <link>https://dev.to/sphranko</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1077108%2Fb37f1ee7-0a6d-4634-a3a0-03e5377205c5.png</url>
      <title>DEV Community: Stefano Franco</title>
      <link>https://dev.to/sphranko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sphranko"/>
    <language>en</language>
    <item>
      <title>php2shell on a dead distro: rebuilding patched PHP for Debian 9 and 8</title>
      <dc:creator>Stefano Franco</dc:creator>
      <pubDate>Mon, 26 Oct 2026 20:37:07 +0000</pubDate>
      <link>https://dev.to/sphranko/php2shell-on-a-dead-distro-rebuilding-patched-php-for-debian-9-and-8-ici</link>
      <guid>https://dev.to/sphranko/php2shell-on-a-dead-distro-rebuilding-patched-php-for-debian-9-and-8-ici</guid>
      <description>&lt;p&gt;On 21 October 2026 the PHP project fixed two critical bugs, &lt;a href="https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w" rel="noopener noreferrer"&gt;CVE-2026-109618&lt;/a&gt; and &lt;a href="https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp" rel="noopener noreferrer"&gt;CVE-2026-109619&lt;/a&gt;, quickly nicknamed &lt;em&gt;php2shell&lt;/em&gt;. Both can allow unauthenticated remote code execution. In the first, an out-of-memory error can interrupt PHP while internal request data is only partially initialized; request parsing is the easiest way to get there, but not the only one. In the second, request-owned state survives in a persistent worker after its memory has been freed. Full RCE was demonstrated on Apache mod_php; on PHP-FPM the exposure is more limited and depends on the configuration, but "limited" is not "safe".&lt;/p&gt;

&lt;p&gt;The fixed releases were 8.2.35, 8.3.36, 8.4.27 and 8.5.12, and the PHP project published minimal source backports for 7.4, 8.0 and 8.1. Anything older depends on distributions and LTS providers. AWS saw a first exploitation attempt against its honeypots on 17 October, four days before disclosure (&lt;a href="https://aws.amazon.com/blogs/security/amazon-threat-intelligence-identifies-php2shell-exploitation-prior-to-public-disclosure-in-madpot/" rel="noopener noreferrer"&gt;AWS&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;If you run PHP from &lt;a href="https://deb.sury.org" rel="noopener noreferrer"&gt;deb.sury.org&lt;/a&gt; on &lt;strong&gt;Debian 9 stretch&lt;/strong&gt; or &lt;strong&gt;Debian 8 jessie&lt;/strong&gt;, there is no patched package for you. Upgrading the operating system is the real fix, but it is weeks of work, and a hole like this one needs to be closed in days.&lt;/p&gt;

&lt;p&gt;This post shows how to do that: find a patched source, rebuild it for a distribution its maintainer no longer supports, avoid the traps along the way, rehearse the installation on a copy of a production server, and roll it out while checking that the pages you serve do not change. The examples use &lt;code&gt;php7.2 7.2.34-66&lt;/code&gt;; other branches need their own compatibility checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 0: read what you actually run
&lt;/h2&gt;

&lt;p&gt;Before choosing anything, read every host, read-only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep &lt;/span&gt;PRETTY_NAME /etc/os-release&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;
php &lt;span class="nt"&gt;-v&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; php7.2 &lt;span class="nt"&gt;-v&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;                                      &lt;span class="c"&gt;# `php` may point to another branch&lt;/span&gt;
dpkg-query &lt;span class="nt"&gt;-W&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'${Package} ${Version}\n'&lt;/span&gt; &lt;span class="s1"&gt;'php7.2*'&lt;/span&gt; &lt;span class="s1"&gt;'libicu*'&lt;/span&gt; &lt;span class="s1"&gt;'libsodium*'&lt;/span&gt; php-common
php-fpm7.2 &lt;span class="nt"&gt;-i&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^(session.upload_progress.enabled|memory_limit)'&lt;/span&gt;   &lt;span class="c"&gt;# FPM defaults, before pool overrides&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; /var/cache/apt/archives | &lt;span class="nb"&gt;grep &lt;/span&gt;php7.2                                   &lt;span class="c"&gt;# rollback material&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/apt/sources.list /etc/apt/sources.list.d/&lt;span class="k"&gt;*&lt;/span&gt;.list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;php-fpm7.2 -i&lt;/code&gt; does not show what a pool or the web server overrides (&lt;code&gt;php_admin_value&lt;/code&gt;, &lt;code&gt;PHP_VALUE&lt;/code&gt;): read the pool configuration too.&lt;/p&gt;

&lt;p&gt;Three questions decide everything else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Is there a package to install?&lt;/strong&gt; As of 26 October 2026, sury's repository answers 404 for stretch and buster. The patched builds exist only for Debian 11, 12 and 13:&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Debian&lt;/th&gt;
&lt;th&gt;7.2&lt;/th&gt;
&lt;th&gt;8.2&lt;/th&gt;
&lt;th&gt;8.3&lt;/th&gt;
&lt;th&gt;8.4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;9 stretch, 10 buster&lt;/td&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11, 12, 13&lt;/td&gt;
&lt;td&gt;7.2.34-66&lt;/td&gt;
&lt;td&gt;8.2.35&lt;/td&gt;
&lt;td&gt;8.3.36&lt;/td&gt;
&lt;td&gt;8.4.27&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Does apt still work?&lt;/strong&gt; On an old host, &lt;code&gt;sources.list&lt;/code&gt; often still points at Debian mirrors that stopped serving stretch long ago, so any &lt;code&gt;apt-get -f install&lt;/code&gt; fails. Whatever you install has to bring its dependencies with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the apt cache still there?&lt;/strong&gt; Some hosts keep the exact &lt;code&gt;.deb&lt;/code&gt; files of every PHP package they run. That is your rollback, if it is there: check files, versions and architecture rather than assuming.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two cheap mitigations can go in right away:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;session.upload_progress.enabled = Off&lt;/code&gt; in &lt;code&gt;php.ini&lt;/code&gt; or in the pool configuration (not with &lt;code&gt;ini_set&lt;/code&gt;: it must apply before the request is parsed), then restart the workers. One of the documented exploitation paths of the second CVE goes through it. On 23 October PHP also recommended keeping it off until the next release because of a further, unlikely vector, &lt;a href="https://www.php.net/archive/2026.php" rel="noopener noreferrer"&gt;CVE-2026-115560&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;If a virtual host only ever needs to answer GET and HEAD requests, make the web server refuse other methods and cap request bodies before PHP sees them:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;  &lt;span class="k"&gt;client_max_body_size&lt;/span&gt; &lt;span class="mi"&gt;1k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request_method&lt;/span&gt; &lt;span class="s"&gt;!~&lt;/span&gt; &lt;span class="s"&gt;^(GET|HEAD)&lt;/span&gt;$&lt;span class="s"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;405&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This limits bodies to 1 KB, it does not reject them. Add OPTIONS only if that virtual host needs it (for example for CORS preflight), and check your access logs first, to be sure nobody legitimately sends anything else.&lt;/p&gt;

&lt;p&gt;These reduce exposure; they do not fix anything. The advisories list further temporary measures and their limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The options
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mitigations only.&lt;/strong&gt; Necessary, not sufficient: they do not close every path to either CVE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paid extended support.&lt;/strong&gt; Freexian publishes patched PHP for old Debian releases, from Debian 9 up; jessie is not on their list (&lt;a href="https://www.freexian.com/lts/php/" rel="noopener noreferrer"&gt;Freexian PHP LTS&lt;/a&gt;). A perfectly good option, if it covers your exact combination and fits your budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Containers on the old hosts.&lt;/strong&gt; The newest Docker CE in Docker's stretch archive is 19.03.15. With the old Docker we tried, the default seccomp profile broke thread creation (&lt;code&gt;clone3&lt;/code&gt;) in images based on recent glibc. Test your exact runtime and image before betting on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jump to PHP 8.x on stretch, compiled from source.&lt;/strong&gt; stretch's libraries meet PHP 8.3's basic build requirements (8.4 needs libcurl ≥ 7.61; stretch has 7.52), but every extension you use has its own. And your application may need changes, or may not run on PHP 8 at all: that is a project, not a patch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebuild sury's patched package for your old Debian.&lt;/strong&gt; On 21 October sury published &lt;code&gt;php7.2 7.2.34-66&lt;/code&gt; with a backport of the fix, and the source package is public. If it builds on stretch, it installs over your current 7.2 with the same package names, the same &lt;code&gt;/etc/php/7.2&lt;/code&gt; and the same &lt;code&gt;php7.2-fpm&lt;/code&gt; service: no application change. That is the route described here.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is inside sury's source package
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dpkg-source &lt;span class="nt"&gt;-x&lt;/span&gt; php7.2_7.2.34-66+0~20261021.122+debian12~1.gbpe0bcd4.dsc php7.2-src
&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; php7.2-src/debian/patches/series   &lt;span class="c"&gt;# 159&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not add &lt;code&gt;--no-check&lt;/code&gt;: it also disables the checksum checks. And a download over HTTPS is not authentication. This &lt;code&gt;.dsc&lt;/code&gt; carries no signature of its own, so authenticate the files through the repository's signed metadata: the &lt;code&gt;InRelease&lt;/code&gt; file (verified with sury's archive key), then the &lt;code&gt;Sources&lt;/code&gt; index it lists, then the checksums of the source files. Keep that metadata next to the files.&lt;/p&gt;

&lt;p&gt;PHP 7.2.34 plus &lt;strong&gt;159 patches&lt;/strong&gt;: six years of security backports and packaging changes. The last one is the fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0159-Avoid-state-corruption-via-bailout.patch
  Minimal mitigation for EOL version.
  Fix GHSA-w8hw-w9fx-j79w (CVE-2026-109618)
  Fix GHSA-6wmf-4rv5-p3rp (CVE-2026-109619)
  Backported from the PHP 7.4 version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It touches 16 files in &lt;code&gt;Zend/&lt;/code&gt;, &lt;code&gt;main/rfc1867.c&lt;/code&gt;, &lt;code&gt;main/SAPI.c&lt;/code&gt; and &lt;code&gt;ext/session/session.c&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Before building anything, check the two things most likely to make the idea fail, in a throwaway &lt;code&gt;debian:stretch&lt;/code&gt; container: do all patches apply, and do the build dependencies resolve on stretch?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dpkg-source &lt;span class="nt"&gt;-x&lt;/span&gt; php7.2_&lt;span class="k"&gt;*&lt;/span&gt;.dsc php7.2-src                    &lt;span class="c"&gt;# applies all patches&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;php7.2-src
mk-build-deps &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="s1"&gt;'apt-get -y --no-install-recommends'&lt;/span&gt; debian/control
dpkg-checkbuilddeps &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"BUILD-DEPS OK"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both pass (stretch's debhelper 10 is new enough). Ten minutes to know the plan is viable.&lt;/p&gt;

&lt;h3&gt;
  
  
  The backport is not upstream's fix
&lt;/h3&gt;

&lt;p&gt;"Minimal mitigation for EOL version" means what it says. Read the patch before shipping it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;when a request hits &lt;code&gt;memory_limit&lt;/code&gt; (and on a few other allocation failures), the patched allocator prints the fatal error to stderr and ends the process with &lt;code&gt;_exit(1)&lt;/code&gt;: no normal request shutdown, no shutdown handlers, no session write, and the message does not go through PHP's error log. Under FPM the worker dies and the master replaces it; enable &lt;code&gt;catch_workers_output&lt;/code&gt; in the pool if you want to see those messages;&lt;/li&gt;
&lt;li&gt;the session upload progress callback returns unconditionally (&lt;code&gt;|| 1&lt;/code&gt;), so that feature is off even if &lt;code&gt;php.ini&lt;/code&gt; says &lt;code&gt;On&lt;/code&gt;. Ordinary file uploads are a different thing and keep working.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A comparison of ordinary pages does not exercise either change. Test uploads, error handling and FPM recovery on the replica.&lt;/p&gt;

&lt;h3&gt;
  
  
  The telemetry patch
&lt;/h3&gt;

&lt;p&gt;Patch &lt;code&gt;0144&lt;/code&gt; is a surprise: &lt;em&gt;"Add minimal privacy-preserving secure DNS telemetry to check current version"&lt;/em&gt;, added on 12 February 2026. It adds &lt;code&gt;main/secure_dns_telemetry.h&lt;/code&gt; and hooks it into the FPM and &lt;code&gt;mod_php&lt;/code&gt; startup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;at every start it builds &lt;code&gt;v1|&amp;lt;package&amp;gt;|&amp;lt;version&amp;gt;|&amp;lt;timestamp&amp;gt;&lt;/code&gt;, encrypts it with a public key, and sends it as a DNS query (UDP 53) to the configured host;&lt;/li&gt;
&lt;li&gt;a reply may carry an advisory, which is only logged if it says "high", "critical" or "emergency" (and both the FPM and the &lt;code&gt;mod_php&lt;/code&gt; hooks define the logging macro as empty);&lt;/li&gt;
&lt;li&gt;nothing received is executed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next day's changelog entry says &lt;em&gt;"The secure DNS telemetry is disabled at the compile-time"&lt;/em&gt;, and &lt;code&gt;debian/rules&lt;/code&gt; says &lt;em&gt;"not enabled in build by default"&lt;/em&gt;. The hostname is not in the patch: the startup call is compiled in only when &lt;code&gt;TELEMETRY_HOST&lt;/code&gt; is defined, and &lt;code&gt;debian/rules&lt;/code&gt; picks it up, if present, from &lt;code&gt;/usr/lib/php/php-common.mk&lt;/code&gt;, shipped by the &lt;code&gt;php-common&lt;/code&gt; installed where the package is built (it is a build dependency). In Debian's own &lt;code&gt;php-common&lt;/code&gt; that file is empty. In the one published on packages.sury.org it contains (checked on 26 October 2026):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nv"&gt;-DTELEMETRY_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'"telemetry.sury.org"'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-DTELEMETRY_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'"53"'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-DTELEMETRY_PK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'"…"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So packages built against sury's &lt;code&gt;php-common&lt;/code&gt; have the call compiled in, and Debian's own builds do not. We found &lt;code&gt;telemetry.sury.org&lt;/code&gt; in an official &lt;code&gt;php-fpm8.1&lt;/code&gt; binary from sury's repository. Check your own binaries (&lt;code&gt;strings /usr/sbin/php-fpm7.2 | grep telemetry&lt;/code&gt;). A project that lives on donations has good reasons to want to know how many installations exist, and the code is careful. We dropped the patch from our build anyway: it is not a security fix, builds from before February 2026 never had it, and even with the call disabled the header is included and FPM is linked against libsodium, which breaks the build on stretch (it needs libsodium ≥ 1.0.14; stretch has 1.0.11). No later patch touches its files, so removing it is clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building for stretch, and the ICU trap
&lt;/h2&gt;

&lt;p&gt;The builder is a Dockerfile on &lt;code&gt;debian:stretch&lt;/code&gt;, with the archived repositories. This is the core of it, not the complete build (the ICU part follows):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; --platform=linux/amd64 debian:stretch&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;      &lt;span class="s1"&gt;'deb http://archive.debian.org/debian stretch main contrib non-free'&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;      &lt;span class="s1"&gt;'deb http://archive.debian.org/debian-security stretch/updates main contrib non-free'&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;      &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/apt/sources.list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'Acquire::Check-Valid-Until "false";'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/apt/apt.conf.d/10no-check-valid-until &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; ca-certificates curl dpkg-dev devscripts &lt;span class="se"&gt;\
&lt;/span&gt;      equivs fakeroot quilt xz-utils

&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; SURY_VERSION=7.2.34-66+0~20261021.122+debian12~1.gbpe0bcd4&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; SURY_POOL=https://packages.sury.org/php/pool/main/p/php7.2&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /build&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSO&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SURY_POOL&lt;/span&gt;&lt;span class="s2"&gt;/php7.2_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SURY_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.dsc"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    curl &lt;span class="nt"&gt;-fsSO&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SURY_POOL&lt;/span&gt;&lt;span class="s2"&gt;/php7.2_7.2.34.orig.tar.xz"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    curl &lt;span class="nt"&gt;-fsSO&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SURY_POOL&lt;/span&gt;&lt;span class="s2"&gt;/php7.2_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SURY_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.debian.tar.xz"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    dpkg-source &lt;span class="nt"&gt;--skip-patches&lt;/span&gt; &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="s2"&gt;"php7.2_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SURY_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.dsc"&lt;/span&gt; php7.2-src &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;cd &lt;/span&gt;php7.2-src &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'/^0144-Add-minimal-privacy-preserving-secure-DNS-telemetry-.patch$/d'&lt;/span&gt; debian/patches/series &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nv"&gt;QUILT_PATCHES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;debian/patches quilt push &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;--fuzz&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;php7.2-src &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    mk-build-deps &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="s1"&gt;'apt-get -y -q --no-install-recommends'&lt;/span&gt; debian/control
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Check-Valid-Until "false"&lt;/code&gt; only accepts the archive's expired release dates; signatures are still checked. If you authenticated the source files elsewhere (see above), copy them in instead of downloading them here.&lt;/p&gt;

&lt;p&gt;The build itself gives the packages a local revision, so they are recognizable on the servers and sort above the old ones (check with &lt;code&gt;dpkg --compare-versions&lt;/code&gt; against what the hosts run, and never reuse a revision for different contents):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dch &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"7.2.34-66+stretch2"&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; stretch &lt;span class="s2"&gt;"Rebuilt for Debian 9 stretch, without the DNS telemetry patch (0144), intl against ICU 64."&lt;/span&gt;
&lt;span class="nv"&gt;DEB_BUILD_OPTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"nocheck parallel=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;nproc&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; dpkg-buildpackage &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nt"&gt;-uc&lt;/span&gt; &lt;span class="nt"&gt;-us&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;nocheck&lt;/code&gt; skips PHP's own test suite: a package that compiles is not a tested package, so the checks later in this post are not optional.&lt;/p&gt;

&lt;p&gt;Our first package built, installed cleanly and served pages. One page was different: Italian dates had turned from &lt;code&gt;7 ott 2026, 17:12&lt;/code&gt; into &lt;code&gt;07 ott 2026, 17:12&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The PHP being replaced had not been linked against stretch's ICU 57. sury's 2019 builds for stretch used a &lt;strong&gt;backported ICU 64&lt;/strong&gt;, together with backported pcre, libsodium, libargon2, libgd and libtidy. ICU 57 carries older locale data (CLDR), which formats Italian medium dates differently. The same library drives localized dates and number formats, and the &lt;code&gt;Transliterator&lt;/code&gt; that many applications use to generate URL slugs, so this is not cosmetic: a slug generated with the new build could differ from the one the old build generated for the same title. &lt;strong&gt;Check which ICU your current &lt;code&gt;intl.so&lt;/code&gt; declares before you build&lt;/strong&gt; (&lt;code&gt;objdump -p … | grep NEEDED&lt;/code&gt;), and record what PHP reports at runtime too.&lt;/p&gt;

&lt;p&gt;We could not find ICU 64 development packages for stretch in any repository or archive we checked, not even in sury's pool. What worked:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install the &lt;code&gt;libicu64&lt;/code&gt; runtime package your servers already have in the builder (copy it from a host's apt cache).&lt;/li&gt;
&lt;li&gt;Build ICU 64.1 from upstream only for its headers and pkg-config files. Check the tarball's GPG signature against unicode-org's &lt;code&gt;KEYS&lt;/code&gt; file (it is signed by ICU's release manager) and pin its SHA-512 in the Dockerfile.&lt;/li&gt;
&lt;li&gt;Replace the libraries of that ICU build with links to the packaged ones, so anything linking "ICU 64" links the library the servers already have.&lt;/li&gt;
&lt;li&gt;Point &lt;code&gt;PKG_CONFIG_PATH&lt;/code&gt; at it, because PHP's &lt;code&gt;configure&lt;/code&gt; asks pkg-config first.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; prod-debs/libicu64_*.deb /tmp/&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;dpkg &lt;span class="nt"&gt;-i&lt;/span&gt; /tmp/libicu64_&lt;span class="k"&gt;*&lt;/span&gt;.deb
&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /tmp/icu.tgz &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ICU_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ICU_SHA512&lt;/span&gt;&lt;span class="s2"&gt;  /tmp/icu.tgz"&lt;/span&gt; | &lt;span class="nb"&gt;sha512sum&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; - &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xzf&lt;/span&gt; /tmp/icu.tgz &lt;span class="nt"&gt;-C&lt;/span&gt; /tmp &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; /tmp/icu/source &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    ./configure &lt;span class="nt"&gt;--prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt/icu64 &lt;span class="nt"&gt;--disable-samples&lt;/span&gt; &lt;span class="nt"&gt;--disable-tests&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make &lt;span class="nt"&gt;-j&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;nproc&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; make &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="k"&gt;for &lt;/span&gt;lib &lt;span class="k"&gt;in&lt;/span&gt; /opt/icu64/lib/libicu&lt;span class="k"&gt;*&lt;/span&gt;.so&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$lib&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="k"&gt;for &lt;/span&gt;lib &lt;span class="k"&gt;in&lt;/span&gt; /usr/lib/x86_64-linux-gnu/libicu&lt;span class="k"&gt;*&lt;/span&gt;.so.64&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;      &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$lib&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; .64&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$lib&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"/opt/icu64/lib/&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$lib&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"/opt/icu64/lib/&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&gt;.64"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PKG_CONFIG_PATH=/opt/icu64/lib/pkgconfig&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;configure&lt;/code&gt; duly reported &lt;code&gt;checking for location of ICU headers and libraries... found 64.1&lt;/code&gt;. The package still declared &lt;code&gt;Depends: libicu57&lt;/code&gt;. Most likely, the linker had resolved &lt;code&gt;-licuuc&lt;/code&gt; through the &lt;strong&gt;development symlinks of libicu-dev 57&lt;/strong&gt; (&lt;code&gt;/usr/lib/x86_64-linux-gnu/libicuuc.so → .so.57&lt;/code&gt;), which come before our directory on its search path: headers from 64, library from 57. Either way, that package was unusable. A post-build check that only reads &lt;code&gt;Depends&lt;/code&gt; would not catch the cases where the two disagree; this one does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; /x &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; dpkg-deb &lt;span class="nt"&gt;-x&lt;/span&gt; php7.2-intl_&lt;span class="k"&gt;*&lt;/span&gt;.deb /x
objdump &lt;span class="nt"&gt;-p&lt;/span&gt; /x/usr/lib/php/20170718/intl.so | &lt;span class="nb"&gt;grep &lt;/span&gt;NEEDED | &lt;span class="nb"&gt;grep &lt;/span&gt;icu   &lt;span class="c"&gt;# must say .so.64&lt;/span&gt;
dpkg-deb &lt;span class="nt"&gt;-f&lt;/span&gt; php7.2-intl_&lt;span class="k"&gt;*&lt;/span&gt;.deb Depends                                  &lt;span class="c"&gt;# must say libicu64&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make the build script fail if either says 57, then check on the replica that the module loads and formats as expected. Our first version of that check also had a bug: it called &lt;code&gt;dpkg-deb&lt;/code&gt; on the Mac, where it does not exist, and fell back to a Docker command that passed the &lt;code&gt;*&lt;/code&gt; unexpanded. Test your tests.&lt;/p&gt;

&lt;p&gt;Retargeting those development symlinks to 64 and moving the ICU 57 headers away fixes the link, and breaks the next build: &lt;strong&gt;libxml2's own headers include &lt;code&gt;&amp;lt;unicode/ucnv.h&amp;gt;&lt;/code&gt;&lt;/strong&gt;, so &lt;code&gt;ext/libxml&lt;/code&gt; stops compiling. The final fix points &lt;code&gt;/usr/include/unicode&lt;/code&gt; at the ICU 64 headers instead of removing it. At runtime libxml2 keeps using ICU 57 and intl uses ICU 64, both loaded in the same process, exactly as with the old build: these ICU builds export their symbols with a version suffix (&lt;code&gt;ucol_open_64&lt;/code&gt;), so the two did not clash. Check your own binaries rather than assuming every ICU build does the same.&lt;/p&gt;

&lt;p&gt;Each build took about 50 minutes under amd64 emulation (Rosetta) on an 8-core Apple Silicon laptop, and three of ours failed: telemetry and libsodium, ICU linked to 57, libxml2 headers. Put everything that can fail fast at the beginning of the Dockerfile, and give each rebuild a new revision (&lt;code&gt;+stretch1&lt;/code&gt;, &lt;code&gt;+stretch2&lt;/code&gt;) so a bad package can never be mistaken for a good one.&lt;/p&gt;

&lt;h2&gt;
  
  
  A production replica to rehearse on
&lt;/h2&gt;

&lt;p&gt;Before touching a server, build a &lt;code&gt;debian:stretch&lt;/code&gt; container that &lt;strong&gt;is&lt;/strong&gt; one of your servers, as far as PHP is concerned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the exact production &lt;code&gt;.deb&lt;/code&gt; files, copied (read-only) from a server's apt cache: PHP packages, pecl extensions and backported libraries, with their checksums;&lt;/li&gt;
&lt;li&gt;the production &lt;code&gt;/etc/php&lt;/code&gt;, copied the same way, with only the changes needed to keep the replica away from production: sessions, databases, queues, mail, and any other outbound connection;&lt;/li&gt;
&lt;li&gt;the same web server version as production, with development virtual hosts.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh your-server &lt;span class="s2"&gt;"cd /var/cache/apt/archives &amp;amp;&amp;amp; tar -cf - php7.2-* php-apcu_* …"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xf&lt;/span&gt; - &lt;span class="nt"&gt;-C&lt;/span&gt; prod-debs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mount the application code and point it at a development database, and the replica serves real pages. A container shares the host's kernel and usually does not run systemd: use it for packages and application behavior, and check service start, reload and log rotation on a real host.&lt;/p&gt;

&lt;p&gt;Then compare. Capture a list of representative pages (home, a few dynamic pages, login, an error page, pages with localized dates), flush the application cache, capture again. &lt;strong&gt;Measure the noise before comparing anything.&lt;/strong&gt; Two captures with the &lt;em&gt;same&lt;/em&gt; PHP will differ in things like debug toolbars, CSP nonces, CSRF tokens, last-modified metadata, timestamps embedded in HTML or JavaScript, and lists whose content varies on its own. Normalize only those fields, keep the raw captures, and compare the rest byte by byte:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;normalize&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/nonce="[^"]*"/nonce=""/g'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/(name="csrf_token" value=")[^"]*/\1/g'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
normalize before/page.html &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; before/page.norm.html
normalize after/page.html  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; after/page.norm.html
cmp before/page.norm.html after/page.norm.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adapt the patterns to your pages, do not squeeze whitespace (it can matter), look at the raw diff too, and check that tokens still work rather than just hiding them. With the right ICU, every page we sampled was identical after normalization, Italian dates included. That is a bounded regression check, not a proof that nothing else changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Six bugs the rehearsal found in our own install script
&lt;/h3&gt;

&lt;p&gt;The rehearsal is not only about the package. Running the exact installation procedure on the replica found six bugs in ours before it ever touched production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;apt-get install debs/x.deb&lt;/code&gt; (relative path) is read as a package name. apt wants &lt;code&gt;./debs/x.deb&lt;/code&gt; or an absolute path.&lt;/li&gt;
&lt;li&gt;macOS &lt;code&gt;tar&lt;/code&gt; adds Apple extended attributes that GNU tar on the server warns about: &lt;code&gt;--no-xattrs --no-mac-metadata&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Missing rollback packages produced a warning and the procedure carried on. Now it stops.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;install&lt;/code&gt; ran even after a failed &lt;code&gt;preflight&lt;/code&gt;. Now &lt;code&gt;preflight&lt;/code&gt; leaves a token that &lt;code&gt;install&lt;/code&gt; requires and consumes, tied to the host and to the exact package set.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;preflight&lt;/code&gt; rewrote the "state to roll back to" every time it ran: after one install and one more preflight, a rollback would have "rolled back" to the new version. Now the original state is written once.&lt;/li&gt;
&lt;li&gt;The rollback looped over &lt;code&gt;dpkg-query&lt;/code&gt; output, which also lists virtual names such as &lt;code&gt;php7.2-apcu&lt;/code&gt; (provided by &lt;code&gt;php-apcu&lt;/code&gt;) with an empty version, and stopped on the first one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And one difference between a replica and production that matters: &lt;strong&gt;official Debian Docker images delete downloaded packages after every apt run&lt;/strong&gt; (&lt;code&gt;/etc/apt/apt.conf.d/docker-clean&lt;/code&gt;). A rollback that relies on the apt cache can work on real servers and silently have nothing to work with in the container. Remove that file in the replica, and on production keep an explicit rollback bundle instead of trusting the cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing without breaking things
&lt;/h2&gt;

&lt;p&gt;The first, naive installation attempt on the replica used &lt;code&gt;dpkg -i&lt;/code&gt;. A dependency was missing (&lt;code&gt;libargon2-0&lt;/code&gt;, which stretch has but the host did not), and dpkg did what dpkg does: it unpacked everything and left the packages unconfigured. The PHP CLI broke immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;php: error while loading shared libraries: libargon2.so.0: cannot open shared object file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything that runs the PHP CLI, such as cron jobs and queue workers, would be down, and package scripts can restart PHP-FPM too. So the installation goes through apt, with apt allowed to see &lt;strong&gt;only the local &lt;code&gt;.deb&lt;/code&gt; files and the installed packages&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;APT_LOCAL&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; Dir::Etc::SourceList&lt;span class="o"&gt;=&lt;/span&gt;/dev/null
  &lt;span class="nt"&gt;-o&lt;/span&gt; Dir::Etc::SourceParts&lt;span class="o"&gt;=&lt;/span&gt;/dev/null
  &lt;span class="nt"&gt;-o&lt;/span&gt; Dir::State::Lists&lt;span class="o"&gt;=&lt;/span&gt;/tmp/apt-empty-lists
  &lt;span class="nt"&gt;-o&lt;/span&gt; Dpkg::Options::&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;--force-confold&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="nt"&gt;--no-remove&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APT_LOCAL&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;debs&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="c"&gt;# simulation&lt;/span&gt;
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="nt"&gt;--no-remove&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APT_LOCAL&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;debs&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a dependency is missing, apt refuses during the solver phase, before unpacking anything; &lt;code&gt;--no-remove&lt;/code&gt; makes it refuse instead of removing packages. That does not make the installation atomic: a full disk or a failing maintainer script can still stop it halfway. Compute the list of packages per host: the new version of every &lt;code&gt;php7.2-*&lt;/code&gt; package installed there, nothing more, plus the runtime libraries that travel with them.&lt;/p&gt;

&lt;h3&gt;
  
  
  preflight
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;preflight&lt;/code&gt; must succeed before &lt;code&gt;install&lt;/code&gt; agrees to run. It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;backs up &lt;code&gt;/etc/php/7.2&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;records the state: package versions, the modules loaded by the CLI &lt;strong&gt;and&lt;/strong&gt; by FPM, and the output of a few intl calls that would expose an ICU change:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;  &lt;span class="nv"&gt;$d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"2026-10-07 17:12:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DateTimeZone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Europe/Rome"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nc"&gt;IntlDateFormatter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SHORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;IntlDateFormatter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MEDIUM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;IntlDateFormatter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;LONG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;IntlDateFormatter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FULL&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IntlDateFormatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"it_IT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;IntlDateFormatter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SHORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Europe/Rome"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NumberFormatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"it_IT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;NumberFormatter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CURRENCY&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;formatCurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1234.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nc"&gt;Transliterator&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Any-Latin; Latin-ASCII; Lower()"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;transliterate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Perché l’università è così?"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;writes the rollback target once, and checks that every package needed to go back is available (copying it from your own set when the host lacks it);&lt;/li&gt;
&lt;li&gt;checks free disk space;&lt;/li&gt;
&lt;li&gt;simulates the installation and &lt;strong&gt;stops on any planned downgrade or removal&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After &lt;code&gt;install&lt;/code&gt;, the recorded state must differ only in what you expect: version numbers, and the behavior changes of the backport described above. Anything else is a reason to roll back.&lt;/p&gt;

&lt;h3&gt;
  
  
  What real hosts can still teach you
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A newer library than yours.&lt;/strong&gt; A host may already have sury's 2019 &lt;code&gt;libargon2-0&lt;/code&gt;, newer than the stretch one you ship. apt refuses the downgrade, with &lt;code&gt;Packages were downgraded and -y was used without --allow-downgrades&lt;/code&gt;, and touches nothing. Install dependencies only when missing or older, and run the downgrade check in &lt;code&gt;preflight&lt;/code&gt;. (Our first version of that check died silently: a &lt;code&gt;dpkg --compare-versions … &amp;amp;&amp;amp; echo&lt;/code&gt; returning 1 inside a &lt;code&gt;while&lt;/code&gt; loop under &lt;code&gt;set -e -o pipefail&lt;/code&gt;. It failed closed, which is the right way to fail.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A missing rollback package.&lt;/strong&gt; Some hosts have nothing in their apt cache, or have packages installed that your rollback set does not cover, such as a &lt;code&gt;-dbgsym&lt;/code&gt; debug package. &lt;code&gt;preflight&lt;/code&gt; should stop; add the missing package to the rollback set, taken from another host's cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A full disk.&lt;/strong&gt; Check free space before installing, not halfway through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;php-fpm.conf&lt;/code&gt; gets replaced&lt;/strong&gt; where it is still the stock file (dpkg updates untouched conffiles), while customized &lt;code&gt;php.ini&lt;/code&gt; files are kept by ucf, with the new version saved as &lt;code&gt;php.ini.ucf-dist&lt;/code&gt;. &lt;code&gt;--force-confold&lt;/code&gt; does not govern ucf. Diff &lt;code&gt;/etc/php&lt;/code&gt; against the backup after the first host, not after the last.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A new syslog line&lt;/strong&gt;: the new systemd unit runs &lt;code&gt;ExecStartPost=-/usr/lib/php/php-fpm-socket-helper&lt;/code&gt;, which an older &lt;code&gt;php-common&lt;/code&gt; does not ship. The leading &lt;code&gt;-&lt;/code&gt; makes systemd ignore the failure, but the socket alias that helper creates does not exist. Harmless only if nothing uses it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rolling it out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start with a single host.&lt;/strong&gt; Capture pages twice before installing (the two captures must be identical after normalization), install, capture again. Then compare the patched host with an unpatched one &lt;strong&gt;at the same moment&lt;/strong&gt;, on pages generated per request (&lt;code&gt;Cache-Control: no-cache, private&lt;/code&gt;), including pages with localized dates. Make sure every request really reached PHP on the host you think it did, past any cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then one host at a time.&lt;/strong&gt; Take it out of the load balancer, wait until its connections drop to zero, capture, &lt;code&gt;preflight&lt;/code&gt;, &lt;code&gt;install&lt;/code&gt;, check that the old workers are gone and the new binary is serving, capture again, compare, put it back, and listen for a few minutes: connections, load, PHP-FPM state, web server errors &lt;strong&gt;by type&lt;/strong&gt;, not by count.&lt;/p&gt;

&lt;p&gt;Two things worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Health checks can lag.&lt;/strong&gt; If your health check is a PHP script toggled by a flag in its source, OPcache may keep serving the old version for up to &lt;code&gt;opcache.revalidate_freq&lt;/code&gt; seconds (when &lt;code&gt;opcache.validate_timestamps&lt;/code&gt; is on). A flag read from a file or a cache has its own rules. Retry for a while before declaring the host back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load goes up for a few minutes after each FPM restart&lt;/strong&gt;, because OPcache and APCu start empty. Expect it, and check it settles without new errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some special cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hosts you cannot drain.&lt;/strong&gt; Reduce what PHP is exposed to first (the request-method rule above), plan for a short interruption, then install in place and compare on real requests, body and significant headers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Old packages that no longer exist.&lt;/strong&gt; If the exact packages a host runs are gone everywhere, you have no exact rollback. Pick a recovery target you can install with the same configuration, test that it works, and remember it is just as vulnerable as the original.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLI-only hosts.&lt;/strong&gt; Patch them too if they run the same application: with a different PHP and ICU, their jobs would generate slugs and dates that differ from the website's. Running processes keep the old binary and extensions in memory until they exit: restart long-running workers, and watch jobs that start during the installation itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Debian 8 jessie
&lt;/h2&gt;

&lt;p&gt;The same source builds for jessie, with more surgery:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;remove the &lt;code&gt;tidy&lt;/code&gt; and &lt;code&gt;zip&lt;/code&gt; extensions if your application does not need them (jessie's libtidy and libzip are older than required): delete their &lt;code&gt;debian/rules.d/ext-*.mk&lt;/code&gt; files, and &lt;code&gt;debian/control&lt;/code&gt; is regenerated without them;&lt;/li&gt;
&lt;li&gt;disable Argon2 password hashing (jessie has no libargon2), after checking that no stored password hash uses it;&lt;/li&gt;
&lt;li&gt;take &lt;code&gt;debhelper&lt;/code&gt; and &lt;code&gt;libsodium-dev&lt;/code&gt; from &lt;code&gt;jessie-backports&lt;/code&gt;, &lt;strong&gt;and nothing else&lt;/strong&gt;, so the packages depend on libraries the server actually has;&lt;/li&gt;
&lt;li&gt;remove one compiler flag: jessie's gcc 4.9 does not know &lt;code&gt;-Wno-error=incompatible-pointer-types&lt;/code&gt;, and with it &lt;code&gt;configure&lt;/code&gt; cannot compile anything;&lt;/li&gt;
&lt;li&gt;jessie's &lt;code&gt;dpkg-buildpackage&lt;/code&gt; writes no &lt;code&gt;.buildinfo&lt;/code&gt; file: adjust whatever copies the results.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;jessie's apt (1.0) cannot install local &lt;code&gt;.deb&lt;/code&gt; files. A tiny local repository solves it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dpkg-scanpackages &lt;span class="nb"&gt;.&lt;/span&gt; /dev/null &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Packages         &lt;span class="c"&gt;# in the builder, next to the .deb files&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'deb [trusted=yes] file:/path/to/debs ./'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; local.list
apt-get &lt;span class="nt"&gt;-o&lt;/span&gt; Dir::Etc::SourceList&lt;span class="o"&gt;=&lt;/span&gt;local.list &lt;span class="nt"&gt;-o&lt;/span&gt; Dir::Etc::SourceParts&lt;span class="o"&gt;=&lt;/span&gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-o&lt;/span&gt; Dir::State::Lists&lt;span class="o"&gt;=&lt;/span&gt;lists &lt;span class="nt"&gt;-o&lt;/span&gt; Dir::Cache&lt;span class="o"&gt;=&lt;/span&gt;cache update
apt-get … &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; php7.2-common php7.2-cli php7.2-fpm …     &lt;span class="c"&gt;# simulation, then the real one&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;[trusted=yes]&lt;/code&gt; skips apt's signature check for that directory: fine for files you already verified and keep in a protected place, never for a repository on the network.&lt;/p&gt;

&lt;p&gt;If the host runs an older minor version, such as 7.1, PHP 7.2 can go in &lt;strong&gt;next to&lt;/strong&gt; it: different package names, separate FPM services, separate sockets. That is still an application upgrade, so test it, and check what happens to the &lt;code&gt;php&lt;/code&gt; CLI alternative too. Nothing changes for visitors until you point the web server's virtual host from &lt;code&gt;php7.1-fpm.sock&lt;/code&gt; to &lt;code&gt;php7.2-fpm.sock&lt;/code&gt; and reload. To be sure requests really go to the new PHP, check the new pool's status page or access log; the I/O counters of the two FPM pools around a few requests are a quick extra signal (the new one goes up, the old one does not move). Rolling back is the same line and another reload; once you are confident, remove the old version, which is still vulnerable.&lt;/p&gt;

&lt;h2&gt;
  
  
  A difference we almost reported as a bug
&lt;/h2&gt;

&lt;p&gt;After the upgrade, &lt;code&gt;php-fpm.conf&lt;/code&gt; contained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;;&lt;span class="n"&gt;pid&lt;/span&gt; = /&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;run&lt;/span&gt;/&lt;span class="n"&gt;php&lt;/span&gt;/&lt;span class="n"&gt;php&lt;/span&gt;@&lt;span class="n"&gt;PHP_MAJOR_VERSION&lt;/span&gt;@.@&lt;span class="n"&gt;PHP_MINOR_VERSION&lt;/span&gt;@-&lt;span class="n"&gt;fpm&lt;/span&gt;.&lt;span class="n"&gt;pid&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commented out, placeholders unsubstituted, while the 2019 package wrote &lt;code&gt;pid = /run/php/php7.2-fpm.pid&lt;/code&gt; and Debian's own packages (8.2 on bookworm, 8.4 on trixie) still do. We traced it to the packaging (a &lt;code&gt;sed&lt;/code&gt; that only rewrites an uncommented &lt;code&gt;pid&lt;/code&gt; line) and drafted a bug report with a one-line fix.&lt;/p&gt;

&lt;p&gt;Then we looked for the project's tracker. The GitHub repository is archived; the tracker moved to Codeberg, and there the change had already been discussed (&lt;a href="https://codeberg.org/oerdnj/deb.sury.org/issues/113" rel="noopener noreferrer"&gt;issue #113&lt;/a&gt;). It is deliberate: the changelog calls it "Do not create obsolete PID file", in the same May 2026 release that hardened the systemd unit. Without a pid file, systemd does not care (&lt;code&gt;Type=notify&lt;/code&gt;, reload via &lt;code&gt;$MAINPID&lt;/code&gt;), and log reopening was rewritten to use &lt;code&gt;systemctl kill&lt;/code&gt; (&lt;a href="https://codeberg.org/oerdnj/deb.sury.org/issues/117" rel="noopener noreferrer"&gt;issue #117&lt;/a&gt;). Only the old init.d scripts need it, and the maintainer said he would rather drop them than patch them. No report needed.&lt;/p&gt;

&lt;p&gt;The same release hardened the systemd unit (&lt;code&gt;ProtectSystem=full&lt;/code&gt;, &lt;code&gt;PrivateDevices=true&lt;/code&gt;, &lt;code&gt;RestrictNamespaces=true&lt;/code&gt; and more; &lt;code&gt;debian/NEWS&lt;/code&gt; lists them and warns that less standard FPM setups may need changes). &lt;code&gt;ProtectSystem=full&lt;/code&gt; makes &lt;code&gt;/usr&lt;/code&gt;, &lt;code&gt;/boot&lt;/code&gt; and &lt;code&gt;/etc&lt;/code&gt; read-only for PHP-FPM, so an application that writes there will break, and an older systemd ignores the options it does not know, with a warning in the journal. Test start, restart, reload and log rotation on the target.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An end-of-life OS turns a five-minute &lt;code&gt;apt upgrade&lt;/code&gt; into a project.&lt;/strong&gt; Budget the migration before the next CVE does it for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read what you run, pools included.&lt;/strong&gt; &lt;code&gt;php-fpm -i&lt;/code&gt; and the pool configuration, not just &lt;code&gt;grep php.ini&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the backport, not just its title.&lt;/strong&gt; A minimal mitigation can change behavior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure the noise before you compare.&lt;/strong&gt; Normalize only what you proved is noise, then compare byte by byte.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check what a binary actually loads&lt;/strong&gt; (&lt;code&gt;NEEDED&lt;/code&gt;), not only what its package declares.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rehearse the installation procedure, not only the package&lt;/strong&gt;, on a replica built from the production packages and configuration. Ours found six bugs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install through apt with local files only&lt;/strong&gt;, simulate first, refuse downgrades and removals, keep the rollback packages next to you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the patches you ship.&lt;/strong&gt; One of them may phone home.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the upstream tracker, and where it lives now, before filing.&lt;/strong&gt; It may be a decision, not a bug.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this replaces upgrading the operating system: it buys you time. But it can close a critical hole in days, with the pages you checked unchanged.&lt;/p&gt;

</description>
      <category>php</category>
      <category>debian</category>
      <category>security</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
