没有不值得去解决的问题,也没有不值得去学习的技术!

Urgent PHP 8.5.12 Upgrade: A 2.5-Hour Production Security Update

[Figure 15: Second production cutover successful; PHP 8.5.12, PHP-FPM, FastCGI, and WordPress all verified]

作者:

在

,

On the evening of October 21, 2026, I started a PHP security update to upgrade the website’s production environment from PHP 8.5.9 to PHP 8.5.12.

I originally thought that a patch version upgrade within the same PHP 8.5 branch wouldn’t take much time.

However, from initial preparation to final deployment, the entire process took about 2.5 hours.

Along the way, I dealt with source compilation, extension rebuilding, production configuration migration, Python compatibility adjustments, standalone PHP-FPM verification, and a nerve-wracking failed production cutover.

Fortunately, the upgrade was ultimately completed successfully. The WordPress site returned to normal, and the original configurations were fully preserved.

This article documents the entire process, including the successful steps, the issues encountered, and my thoughts on improving the next upgrade workflow.

1. Why an Immediate PHP Upgrade Was Necessary

It all started with a security advisory pre-announced by the official PHP team.

On October 20, 2026, the PHP team announced in advance that an important security update would be released on October 21 at 14:00 UTC.

The advisory specifically mentioned that this update addressed two critical vulnerabilities, both scoring 9.2 on the CVSS v4.0 scale.

The official recommendation was for PHP users to prepare in advance so they could upgrade as quickly as possible once the new version was released.

[Figure 1: PHP officially pre-announces security update; both vulnerabilities score 9.2 on CVSS v4.0]
[Figure 1: PHP officially pre-announces security update; both vulnerabilities score 9.2 on CVSS v4.0]

On October 21, the official team published the vulnerability details and released security fix versions for multiple PHP branches:

  • PHP 8.5.12
  • PHP 8.4.27
  • PHP 8.3.36
  • PHP 8.2.35

This fix addressed two critical vulnerabilities that could allow unauthenticated remote code execution:

CVE-2026-109618: Related to the state of internal request data during out-of-memory handling.

CVE-2026-109619: Related to residual global state after memory release, which could be used by subsequent requests if the data is not properly reset.

The official advisory specifically noted that the risk affected not just the Apache PHP module, but also PHP runtime environments handling web requests such as PHP-FPM and php-cgi.

Since my website uses Nginx + PHP-FPM, I naturally needed to take this update seriously.

[Figure 2: PHP officially announces two critical remote code execution vulnerabilities and advises users to update immediately]
[Figure 2: PHP officially announces two critical remote code execution vulnerabilities and advises users to update immediately]

At the time, the production environment was running PHP 8.5.9, so the upgrade goal was clear: update to PHP 8.5.12 as quickly as possible while preserving existing website configurations and runtime capabilities.

Official PHP announcement:

https://www.php.net/archive/2026.php

2. Why Not Overwrite the Existing PHP Directly?

The web server runs Alibaba Cloud Linux 3, and PHP was installed via source compilation at the following location:

/usr/local/php

This environment is managed through OneinStack-related scripts and already contains some long-standing configuration adjustments.

If I had compiled and installed the new version directly into the original directory, any configuration overwrites, extension incompatibilities, or PHP-FPM startup failures could have impacted the running website.

Therefore, instead of overwriting the production PHP from the start, I chose to compile and verify the new version in an isolated directory first.

1. Compiling PHP 8.5.12

First, I downloaded the official PHP source package and verified its SHA-256 checksum.

Next, I recompiled it using the PHP 8.5.12 source, referencing the compilation parameters from the current production environment.

During compilation, I also had to resolve an iconv header file conflict in the build environment. I ultimately completed the build by isolating and overriding the header files, avoiding direct modifications to system headers.

After compilation, I verified that both the CLI binary and the PHP-FPM binary reported version PHP 8.5.12.

[Figure 3: PHP 8.5.12 source compilation successful; original production PHP 8.5.9 still running normally]
[Figure 3: PHP 8.5.12 source compilation successful; original production PHP 8.5.9 still running normally]

2. Installing to an Isolated Directory

To ensure the new version wouldn’t affect the website prematurely, I used INSTALL_ROOT during installation.

After installation, the new PHP was located at:

/usr/local/src/php85-security-20261021/stage/usr/local/php

This stage only prepared the new version’s binaries; it did not modify the production directory.

[Figure 4: PHP 8.5.12 successfully installed to isolated directory; CLI and PHP-FPM version verification passed]
[Figure 4: PHP 8.5.12 successfully installed to isolated directory; CLI and PHP-FPM version verification passed]

However, there is an easily overlooked issue here.

An isolated installation directory does not mean that PHP’s runtime compile prefix will automatically change as well.

The new PHP’s compile prefix remained /usr/local/php.

This means that before the final cutover, when testing with the isolated binaries, I needed to explicitly specify the corresponding configuration, extension load paths, and other parameters.

This detail also affected subsequent extension compilation and configuration verification.

3. Recompiling Three PHP Extensions

The production environment used three dynamic extensions that needed to be preserved:

LDAP, Imagick, and Redis.

Although this was only a patch upgrade within the PHP 8.5 branch, I decided to rebuild these extensions using the newly compiled PHP environment rather than directly copying the module files from the old version.

This ensured I could verify whether the extensions were compatible with the target PHP runtime.

1. LDAP

I started with LDAP.

To point the extension build tools to the isolated PHP 8.5.12 installation, I needed to prepare corresponding copies of the phpize and php-config tools.

After that, I completed the configuration, compilation, and load testing for the LDAP extension.

[Figure 5: LDAP extension recompilation complete and successfully loaded by PHP 8.5.12]
[Figure 5: LDAP extension recompilation complete and successfully loaded by PHP 8.5.12]

2. Imagick

Next, I recompiled Imagick 3.8.1.

Here, I not only needed to confirm that the PHP extension itself could load, but also check the ImageMagick library it depends on.

Test results showed that Imagick loaded normally in the PHP 8.5.12 environment, using ImageMagick version 7.1.1-10.

[Figure 6: Imagick 3.8.1 recompilation successful; ImageMagick dependency and extension load verification passed]
[Figure 6: Imagick 3.8.1 recompilation successful; ImageMagick dependency and extension load verification passed]

3. Redis

Finally, I handled Redis 6.3.0.

As with the previous two extensions, I compiled it first and then verified module loading using the new PHP.

[Figure 7: Redis 6.3.0 extension recompiled and successfully loaded]
[Figure 7: Redis 6.3.0 extension recompiled and successfully loaded]

At this point, all three dynamic extensions had passed testing.

However, I did not deploy the new PHP immediately.

There was still a trickier issue to address: how to preserve the already-tuned production configurations?

4. Production Configuration Migration Was More Complex Than Expected

For this upgrade, I didn’t want to simply copy the entire old php.ini into the new version.

The reason isn’t that the old configurations were necessarily problematic, but rather that doing so could carry over unsupported parameters, historical default values, and legacy version settings into the new environment.

Conversely, relying entirely on the new version’s default configurations could result in losing the production environment’s tuning parameters.

For example, the current production environment includes:

  • memory_limit = 512M
  • max_execution_time = 600
  • upload_max_filesize = 128M
  • post_max_size = 128M
  • opcache.memory_consumption = 192
  • opcache.interned_strings_buffer = 32
  • pm.max_children = 14

These configurations couldn’t just be carelessly lost during the upgrade.

1. Back Up Production Configurations First

I first saved the current configuration files for PHP, OPcache, and PHP-FPM.

This produced a production configuration snapshot of 7 files, and I recorded their contents using SHA-256 checksums.

[Figure 8: Production PHP, OPcache, and PHP-FPM configuration snapshot created successfully, containing 7 files in total]
[Figure 8: Production PHP, OPcache, and PHP-FPM configuration snapshot created successfully, containing 7 files in total]

2. Auditing 151 Configuration Items

To avoid directly overwriting configurations, I used a configuration migration tool to organize the sources of the old configurations and decide how to handle each item individually.

Ultimately, 151 parameters were identified:

CategoryCountHandling Principle
Default configuration86Use official PHP 8.5.12 template
Production configuration30Preserve existing tuned values
Deployment configuration34Preserve existing deployment requirements
Removed configuration1Not migrated after audit

Additionally, I found 9 instances of duplicate assignments with different values, requiring confirmation of which one actually took effect.

For example, opcache.consistency_checks in the old configuration was no longer supported in PHP 8.5, so it could not be migrated as-is.

After the audit, I generated a formally approved configuration migration strategy.

[Figure 9: Audit of 151 configuration migration categories complete; 9 override priorities and three extension artifacts confirmed]
[Figure 9: Audit of 151 configuration migration categories complete; 9 override priorities and three extension artifacts confirmed]

This process was much more complex than initially anticipated.

But it at least confirmed that no production parameters truly needing preservation were overlooked when the new version went live.

3. The Migration Tool Itself Needed Fixing

Then, two more issues arose.

First, the server’s system Python was version 3.6.8, but the migration tool initially used syntax that required a newer Python version.

I considered installing Python 3.11.

However, after reviewing the installation plan, I found that in addition to adding the Python package, it would also upgrade the system’s chkconfig.

I didn’t think it was necessary to modify system components just to perform a PHP configuration migration.

Therefore, I ultimately chose to make the migration tool compatible with Python 3.6.8.

Subsequently, during the configuration generation phase, I hit another blocker:

php/engine: unknown/removed/unavailable target directive

Further investigation revealed that although engine existed in the official PHP 8.5.12 configuration template, the current runtime did not register this parameter.

This wasn’t an isolated case.

So the issue couldn’t be solved simply by deleting engine; the migration tool’s conditional logic needed to be more accurate.

The final adjustment was: for parameters explicitly audited as default configurations that perfectly matched valid assignments in the target official template, the new template could be used; other unknown parameters still needed to be blocked.

After making the fixes, the regression test results showed 33 tests executed, with 28 passing, 5 skipped, and 0 failures.

[Figure 10: After fixing the configuration migration tool, regression testing completed with no failures]
[Figure 10: After fixing the configuration migration tool, regression testing completed with no failures]

After that, I used the server’s actual Python 3.6.8 to verify the configuration manifest and completed the generation of the PHP 8.5.12 candidate configuration.

All 151 parameters passed validation, and the three extension artifacts also completed integrity checks.

[Figure 11: PHP 8.5.12 candidate configuration generation and validation passed; all 151 parameters handled]
[Figure 11: PHP 8.5.12 candidate configuration generation and validation passed; all 151 parameters handled]

5. Independently Verifying PHP-FPM, Not Just Checking Version Numbers

Completing the compilation and configuration migration didn’t guarantee that PHP-FPM would actually serve web requests normally.

To verify further, I started PHP 8.5.12’s PHP-FPM using an independent Unix socket.

This stage did not take over production requests, nor did it replace the running PHP 8.5.9.

I then sent real FastCGI requests to the new PHP-FPM to check the following:

  • Whether the PHP version was 8.5.12.
  • Whether requests were executed by the www user.
  • Whether production tuning parameters were correctly loaded.
  • Whether OPcache was available.
  • Whether LDAP, Imagick, and Redis loaded normally.
  • Whether all INI files in the configuration directory were read.

Ultimately, all checks passed.

[Figure 12: Standalone PHP-FPM worker successfully handles FastCGI requests; configuration and three extensions verified]
[Figure 12: Standalone PHP-FPM worker successfully handles FastCGI requests; configuration and three extensions verified]

By this point, I was fairly confident.

But before actually switching the production version, I still needed to leave myself a way out.

6. Backing Up PHP 8.5.9 and Preparing for Official Deployment

In addition to the configuration snapshot saved earlier, I also backed up the complete old PHP installation files.

The resulting PHP 8.5.9 rollback archive was about 376MB, and I completed SHA-256 and key file verification.

[Figure 13: Production PHP 8.5.9 rollback backup created successfully; key file and archive integrity verification passed]
[Figure 13: Production PHP 8.5.9 rollback backup created successfully; key file and archive integrity verification passed]

Next, I organized the PHP 8.5.12 binaries, audited configurations, and the three extensions into a complete deployment-ready directory.

During this process, I also noticed that the permissions for two configuration subdirectories in the new directory were 700, whereas the production environment uses 755.

After fixing the permissions, I ran the PHP 8.5.12 load test again as the actual www user, and the result was normal.

At this point, compilation, extensions, configuration, PHP-FPM, and rollback backups had all been verified.

I then proceeded with the official cutover.

7. First Production Cutover Failed, and Automatic Rollback Also Reported Failure

The procedure for the first cutover wasn’t complicated:

Stop PHP 8.5.9’s PHP-FPM, preserve the old installation directory, move PHP 8.5.12 to /usr/local/php, and finally start the new PHP-FPM.

The initial directory switch went smoothly.

But after starting the new PHP-FPM, the script entered an automatic rollback.

The terminal displayed:

Plaintext
FAILED_STAGE=START_NEW_FPM
RESULT=CRITICAL
ROLLBACK=FAILED
ACTION=MANUAL_RECOVERY_REQUIRED
[Figure 14: First production cutover reports failure at starting new PHP-FPM stage; automatic rollback also returns failure]
[Figure 14: First production cutover reports failure at starting new PHP-FPM stage; automatic rollback also returns failure]

Seeing RESULT=CRITICAL was definitely nerve-wracking.

Because at that moment, the most important thing to confirm wasn’t why the new version failed, but whether the website had already returned to normal.

So, instead of immediately rerunning the deployment, I checked the actual server status.

The findings were:

  • /usr/local/php had already reverted to PHP 8.5.9.
  • PHP 8.5.12 was preserved in the failed version directory.
  • The PHP-FPM service was in a normal running state.
  • Actual FastCGI requests passed.
  • The WordPress origin /wp-json/ returned HTTP 200.

In other words, the script reported an automatic rollback failure, but the actual production service had already returned to normal.

This also shows that the status reported by an automation script doesn’t necessarily equal the system’s final state. Especially when a production cutover fails, you can’t rely solely on the last line of output to determine if the website is truly inaccessible.

Where Might the Problem Have Been?

I then checked the systemd logs and found that both the old and new PHP-FPM showed startup records during the cutover, and these operations occurred within a very close timeframe.

Combined with the cutover script’s implementation, I suspected the issue was related to the timing of the health check after service startup.

The original script checked the service and socket status immediately after systemctl start php-fpm returned.

However, the service manager considering the startup operation complete did not necessarily mean that PHP-FPM was immediately ready to handle requests.

If the check happened too early, it could have led to a misjudgment.

However, this was just a judgment based on available evidence, and it didn’t fully prove the root cause of the first failure.

Therefore, the focus of subsequent modifications wasn’t to relax all validation conditions, but to make the startup check truly wait for the service to be ready.

8. Second Cutover: Waiting for Real Requests to Succeed

After the first failure, I didn’t recompile PHP or regenerate the configuration.

I had already confirmed that the PHP 8.5.12 deployment file integrity check still passed, and the old PHP 8.5.9 had been restored normally.

So I only adjusted the cutover procedure.

The most important change was adding a time-limited wait after starting the new PHP-FPM, and using real FastCGI requests for validation.

Only when the new version could actually handle requests, and the returned PHP version, extensions, and configuration all met expectations, was the service considered ready.

The second cutover still retained failure rollback capabilities, but didn’t repeat the extensive preparatory work from before.

This time, the results passed smoothly:

Plaintext
RESULT=PASS
STAGE=PHP8512_PRODUCTION_CUTOVER
PRODUCTION_PHP=8.5.12
FPM=ACTIVE
FASTCGI=PASS
WORDPRESS=PASS
EXTENSIONS=3/3
[Figure 15: Second production cutover successful; PHP 8.5.12, PHP-FPM, FastCGI, and WordPress all verified]
[Figure 15: Second production cutover successful; PHP 8.5.12, PHP-FPM, FastCGI, and WordPress all verified]

After the upgrade, I independently inspected the production environment once more.

The results showed:

  • The PHP version had been updated to 8.5.12.
  • The PHP-FPM master process was running normally.
  • The original 512M memory limit was still in effect.
  • LDAP, Imagick, and Redis all loaded normally.
  • The WordPress origin dynamic interface returned HTTP 200.
  • The complete installation directory for PHP 8.5.9 was still preserved, available for recovery if necessary.

At this point, the security update was truly complete.

9. Why Did a Patch Upgrade Take 2.5 Hours?

Looking back at the whole process, I think the reason goes beyond just the time spent on PHP source compilation and extension building.

More importantly, there wasn’t already a fully validated, reusable upgrade workflow in place.

Many issues were only discovered and resolved step by step during actual execution.

For example:

The configuration migration tool was incompatible with the server’s Python version and required ad-hoc adjustments.

Some parameters in the official configuration template couldn’t be directly validated against the runtime registry, requiring fixes to the migration rules.

Extensions needed recompiling and validating, with proper handling of the isolated installation paths.

The deployment-ready directory also needed permission checks to ensure the www user could read the configurations.

The first official cutover was interrupted due to readiness check issues, and I subsequently had to confirm that the old service had indeed returned to normal.

Individually, each of these tasks was necessary.

But if the next PHP patch upgrade requires going through all of this again, it would be a huge waste of time.

Especially for a web server that is already running normally, I would rather focus my effort on areas of genuine value rather than constantly patching together complex validation scripts.

10. How Should the Next Upgrade Be Improved?

Although this upgrade took 2.5 hours, it also produced quite a few reusable assets.

For example:

The completed PHP configuration migration tool can continue to handle the relationship between the new version’s default configurations and existing production tuning.

The extension building and verification process now has a practical record.

The pre-deployment integrity checks, real FastCGI validation, old version directory preservation, and failure rollback have all been tested in actual use.

Next, I plan to go back to the oneinstack-custom repository to organize and refine these scripts.

However, this experience also prompted further consideration regarding website availability.

The currently successful approach still involves stopping the old PHP-FPM, switching directories, and starting the new PHP-FPM, which means brief request interruptions might occur during the cutover.

For future upgrades, I would prefer to let the old and new PHP-FPM run in parallel temporarily, verify the new service first, and then smoothly route requests to it via Nginx.

If I can implement this approach with minimal changes based on the existing tools, there’s an opportunity to reduce both upgrade time and website downtime.

But this part is currently just a future improvement goal, not a feature completed in this session.

I also don’t intend to spend a lot of time on complex refactoring just to pursue a theoretically perfect solution.

Reuse what can be reused, simplify what can be simplified, and prioritize solving problems that genuinely affect upgrade efficiency and reliability.

11. Summary

This upgrade from PHP 8.5.9 to PHP 8.5.12 was ultimately completed successfully.

From the official announcement of critical security vulnerabilities to completing source compilation, extension verification, production configuration migration, and final deployment, the entire process took about 2.5 hours.

The first cutover failed midway, and the automatic rollback reported a critical error, but subsequent checks confirmed that the production environment had recovered.

After adjusting the service readiness validation method, the second cutover succeeded, and the website is now officially running PHP 8.5.12.

This experience reinforced one thing for me:

The value of an upgrade workflow lies not just in whether it can succeed once, but in whether it can reduce repetitive effort and unnecessary complexity the next time it’s executed.

For critical security vulnerabilities, updating promptly is of course the primary goal.

But from a long-term maintenance perspective, a reliable, simple, and repeatable upgrade workflow is equally worthy of investment.

Next, I will continue to refine the upgrade tools in the repository.

Once the improvements are actually completed and verified, I will separately document the optimization results of this upgrade workflow.


Related links:

Official PHP website: https://www.php.net/

PHP 2026 security announcements: https://www.php.net/archive/2026.php

OneinStack custom repository: https://github.com/shuijingwan/oneinstack-custom

系列导航

关于作者 · 持续学习与技术实践

我是一名拥有 15 年以上开发经验的技术从业者,自 2013 年起持续运营个人技术博客,记录工作与个人项目中遇到的真实问题,以及解决问题过程中的探索、实践和思考。

除了技术写作,我也在持续开发和维护自己的独立项目,探索新技术和新工具在实际场景中的应用。我始终相信,没有不值得去解决的问题,也没有不值得去学习的技术。

如果这篇文章对你有所帮助,欢迎浏览博客中的其他文章,也欢迎交流技术经验与想法。

关于我  ·  GitHub  ·  邮件联系