Resolved PHP 7.2.34 build for Debian 13 (Oct 2025): OPcache optimizer miscompiles loop comparison, causes infinite loop in WordPress core

ajeje

New Pleskian
Server operating system version
Debian 13.5
Plesk version and microupdate number
Plesk Obsidian 18.0.78 #3
Environment:
  • Plesk Obsidian 18.0.78 on Debian 13.5 (fresh install)
  • PHP 7.2.34 from Plesk packages, Build Date: Oct 22 2025 13:42:41, FPM/FastCGI handler
  • WordPress 5.5.x site migrated from Plesk Obsidian 18.0.78 on Debian 10.13 with PHP 7.2.34 (Build Date: Nov 8 2021), where the same site works correctly

Symptom:
After migrating a WordPress site (identical files and database), any wp-admin request hangs for 30s and dies with Maximum execution time exceeded or Allowed memory size exhausted. The log fills with millions of notices (hundreds of MB per request):

Code:
PHP Notice: Undefined offset: 9 in .../wp-includes/formatting.php on line 766
PHP Notice: Undefined offset: 11 in .../wp-includes/formatting.php on line 766
PHP Notice: Undefined offset: 13 in .../wp-includes/formatting.php on line 766
... (offsets keep growing by 2 up to several million until the request is killed)

Root cause analysis:
Line 766 is inside wp_replace_in_html_tags() (stock WordPress core, unmodified):

PHP:
for ( $i = 1, $c = count( $textarr ); $i < $c; $i += 2 ) {
if ( false !== strpos( $textarr[ $i ], $needle ) ) {

I instrumented the loop with a probe logging $i, $c and count($textarr) on the first missing key. Result:

Code:
i=9 c=9 realcount=9

The loop body executes with $i = 9 and $c = 9, i.e. the condition 9 < 9 evaluates as
true. The comparison never becomes false, so the loop runs forever on a 9-element array. The triggering call is harmless: a 411-byte string passed through wpautop() (Yoast SEO admin notification).

Notes for triage:
  • The bug does not reproduce with the same functions copied into a standalone script on the same server — it only manifests inside the full WordPress request, which suggests an OPcache optimization issue dependent on compilation context rather than a PCRE or data problem.
  • PCRE was ruled out: preg_split() with WordPress's get_html_split_regex() returns correct, hole-free arrays in isolated tests (JIT on and off).
  • Same files + same database on the Debian 10 build (Nov 2021) of the same PHP 7.2.34: no issue.

Workarounds verified:
  1. opcache.enable=0 in .user.ini → site works immediately (original WordPress core untouched).
  2. Switching the domain to Plesk PHP 7.4.33 with OPcache enabled → site works.

Suspected cause:
the PHP 7.2 source tree rebuilt in 2025 with Debian 13's modern toolchain (GCC 14) produces a miscompiled OPcache optimizer / VM comparison path. PHP 7.2 is EOL upstream, so this would need a fix in the Plesk build (e.g. compiling with older optimization flags or patching the optimizer).
 
I discovered this problem on some sites when moving from CentOS 7 to AlmaLinux 10. CentOS 7 with PHP 5.6 or 7.x versions older than 7.4 gets a broken opcache in some PHP functions.

I think all EOL versions have this issue, some prestashop modules cause this.

It is important to note that these legacy versions shouldn't even exist on modern systems; they were only added due to user complaints regarding backward compatibility, not have easy solution.

I've had this problem for 7 months xd
 
Thank you for your patience up to this point. According to our devs this it is a bug in PHP 7.2's OPcache optimizer, not in the Plesk build or the compiler.

The OPcache optimizer in PHP 7.2 runs a sequence of passes over the compiled bytecode. The issue happens in interaction between two specific passes:

  • Pass 6: DFA (Data Flow Analysis): analyzes the op array and, among other things, marks variables whose values are statically known at a given point. In this case, it marks $c as a constant with value 9 after the count() call.
  • Pass 8: SCCP (Sparse Conditional Constant Propagation): runs after DFA on the already-transformed bytecode. Seeing $i < 9 at loop entry where $i = 1, it incorrectly concludes the condition is always true and converts the conditional jump into an unconditional one. The loop exit is removed from the bytecode entirely.
The result is a for loop that can never terminate at the bytecode level, regardless of what $i reaches at runtime, which matches the observed behavior exactly (i=9 c=9 still looping).

This is a known class of bugs in SCCP implementations: the pass fails to account for the fact that $i is modified inside the loop body, and over-propagates the constant. PHP 7.2 is EOL since November 2019 and this will not be fixed upstream.

This is not specific to Debian 13 or GCC 14. The bug is in the OPcache optimizer logic and can in principle be triggered on any platform given the right input. The specific string processed by wp_replace_in_html_tags() happens to produce the array size and type that activates this path.

Immediate workaround:

Add the following to the domain's .user.ini or the PHP 7.2 FPM pool configuration:

Code:
; Disable OPcache SCCP pass (pass 8) to avoid DFA+SCCP interaction bug in PHP 7.2opcache.optimization_level=0x7FFFBF7F

This disables only the SCCP pass leaving the rest of the optimizer active, and resolves the infinite loop without disabling OPcache entirely.

Long-term recommendation:

PHP 7.2 reached EOL in November 2019. No security or correctness fixes are published upstream. The simplest resolution is to migrate the site to PHP 7.4 or much newer, where this bug does not exist.
 
Although it's highly recommended to upgrade to a new PHP version, if you feel you absolute must use PHP 7.2 (or any EOL version for that matter), you could always consider installing these deprecated PHP versions from a deferent repository with back-ported fixes. Like those provided on Plesk by Tuxcare or alternatively (since you're on Debian) Freexian or Sury.
 
Back
Top