[php-src] Issue #22214: opcache.dups_fix is honored for duplicate classes but not duplicate functions

[email protected] (sibidharan) Tue, 2 Jun 2026 22:39:30 +0000
Newsgroups php.bugs
Message-ID <[email protected]>
Issue: https://github.com/php/php-src/issues/22214
Author: sibidharan

### Description

`opcache.dups_fix` is documented as a fix for "Cannot redeclare" errors, but it only covers classes, not functions.

Inside opcache the directive is read in the table-copy that installs a cached script's symbols (`ext/opcache/zend_accelerator_util_funcs.c`). The class-table copy honors it — when `ignore_dups` is set it keeps the existing class and skips the duplicate:

```c
/* _zend_accel_class_hash_copy */
} else if (UNEXPECTED(!ZCG(accel_directives).ignore_dups)) {
    ...
    zend_class_redeclaration_error(E_ERROR, Z_PTR_P(t));
    return;
}
continue; /* ignore_dups: keep the first definition */
```

The function-table copy right next to it doesn't check the directive at all — it goes straight to the fatal:

```c
/* _zend_accel_function_hash_copy */
t = zend_hash_find_known_hash(target, p->key);
if (UNEXPECTED(t != NULL)) {
    goto failure; /* -> "Cannot redeclare function ..." regardless of opcache.dups_fix */
}
```

So with `opcache.dups_fix=1` a duplicate **class** is tolerated (first wins) but a duplicate **function** still fatals. The directive name and docs don't distinguish between the two, so this reads like an oversight rather than something intentional.

### Where it bites

Long-running application servers that re-execute `require_once`'d files per request (we ran into this building [ZealPHP](https://github.com/sibidharan/zealphp), an OpenSwoole-based runtime). opcache re-installs a cached script's symbols into a table that already has them; `dups_fix` covers the class collision, but the function collision still kills the request with "Cannot redeclare function". So `dups_fix` only half-solves it for these setups — WordPress for example gets past its class redeclares with `dups_fix=1` but then dies on the first function (`_wp_can_use_pcre_u` in `wp-includes/compat.php`).

### Suggested fix

Make the function copy consistent with the class copy:

```diff
 		t = zend_hash_find_known_hash(target, p->key);
 		if (UNEXPECTED(t != NULL)) {
-			goto failure;
+			/* Honor opcache.dups_fix for functions too — the class-table
+			 * copy above already does. Keep the first-declared function. */
+			if (!ZCG(accel_directives).ignore_dups) {
+				goto failure;
+			}
+			continue;
 		}
```

I've tested this against 8.4 and it does the job (WordPress runs clean under opcache + a per-request re-execution model with it). Happy to open a PR with a `.phpt` if the asymmetry is agreed to be unintended — mostly wanted to check whether it's deliberate before sending one.

### PHP Version

PHP 8.4 (the code is the same on master)

### Operating System

Linux