A security update inside one PHP branch, for example 8.3.x to 8.3.36, usually keeps the same extension ABI. A move between branches, such as 7.4 to 8.3, does not. Plan those as different projects. PHP2Shell (CVE-2026-109618 and CVE-2026-109619, disclosed 21 October 2026) pushes many teams toward an update, and the extension check is where most “simple” updates fail.

This guide shows how to list what is loaded, separate packaged modules from custom ones, rebuild what must be rebuilt, and fix the common load errors.

Prerequisites and applicable environments {#prerequisites}

  • Root or sudo on the host, and a staging copy if you can build one.
  • The target version: a fixed upstream release (8.2.35, 8.3.36, 8.4.27, or 8.5.12) or a matched vendor package.
  • A list of third-party extensions and loaders your application needs.
  • A rollback plan. See the rollback plan guide.
IMPORTANT

PHP 8.1 and older get no upstream updates. Vendor backports are possible, and minimal source backports exist for 7.4, 8.0, and 8.1 only. There is no upstream patch for 5.6. If your extension set forces you to stay on an old branch, treat it as a migration problem with temporary mitigations. WAF and ini mitigations are not complete fixes.

What breaks and why {#what-breaks}

PHP extensions compiled as shared objects (.so) are tied to an API and ABI. Two numbers matter.

Term What it is Changes when
PHP API number Version of the internal extension API A new minor branch (for example 8.2 to 8.3)
Zend Module API Version of the Zend engine module interface A new minor branch
Thread safety (NTS or ZTS) How the interpreter was built A different build flavor or package
Debug flag Whether the build includes debug code A debug build is installed

An extension built for one combination refuses to load on another. Within a branch, a patch update normally keeps all four stable. Across branches, assume every custom .so must be rebuilt.

Loaded extension Packaged module Updates with the package Custom .so (PECL, built) Rebuild with phpize Third-party loader Vendor build required

Step-by-step procedure {#procedure}

Step 1. List modules per SAPI {#step-list-modules}

READ-ONLY

php -m | sort > /tmp/modules-cli.txt
wc -l /tmp/modules-cli.txt

php -m on the CLI is a hint. It can miss a module that is enabled only in the FPM or Apache ini. To see the web SAPI list, call the FPM binary directly with its own config.

Debian and Ubuntu:

sudo php-fpm8.3 -m | sort > /tmp/modules-fpm.txt
diff /tmp/modules-cli.txt /tmp/modules-fpm.txt

RHEL, AlmaLinux, Rocky Linux:

sudo php-fpm -m | sort > /tmp/modules-fpm.txt
diff /tmp/modules-cli.txt /tmp/modules-fpm.txt
Illustrative output12a13
> redis
27a29
> imagick

Lines starting with > exist only in the FPM list. Those are the modules the CLI check would have missed. Save both files with the ticket.

Step 2. Find where each module comes from {#step-find-origin}

READ-ONLY

php -i | grep -E '^(extension_dir|Loaded Configuration File|Scan this dir)'
ls -l "$(php -r 'echo ini_get("extension_dir");')"

Then ask the package manager who owns each file.

Debian and Ubuntu:

for f in "$(php -r 'echo ini_get("extension_dir");')"/*.so; do
  if pkg=$(dpkg -S "$f" 2>/dev/null); then printf '%s\t%s\n' "$f" "${pkg%%:*}"; else printf '%s\tNOT PACKAGED\n' "$f"; fi
done

RHEL, AlmaLinux, Rocky Linux:

for f in "$(php -r 'echo ini_get("extension_dir");')"/*.so; do
  if pkg=$(rpm -qf "$f" 2>/dev/null); then printf '%s\t%s\n' "$f" "$pkg"; else printf '%s\tNOT PACKAGED\n' "$f"; fi
done

Anything marked NOT PACKAGED is a custom build and needs a rebuild plan. Check any extension directory used by an extra ini file as well, such as /opt/ or /usr/local/lib/php/.

Step 3. Classify each extension {#step-classify}

Use a sample table like this one. Names are illustrative, and the status column is yours to fill from evidence.

Extension Origin Same-branch update Branch change Notes
mysqli, pdo_mysql Distro package Updates with package Updates with package Install the matching php8.x-mysql set.
gd, curl, mbstring Distro package Updates with package Updates with package Check the same package set is installed.
redis (PECL) Package or PECL build Check origin Rebuild or reinstall for the target Use the packaged module where available.
imagick Package or PECL build Check origin Rebuild or reinstall for the target Depends on system ImageMagick libraries.
In-house .so Custom build Re-test Rebuild required Rebuild against the target headers.
ionCube, SourceGuardian loaders Third-party Vendor check Vendor build required Not assessed in this review.
LIMIT

ionCube, SourceGuardian, and similar loaders are third-party and were not assessed in this review. If the site depends on one, check that vendor's build for the target PHP before the window.

Step 4. Install the target packages {#step-install}

SERVICE

Package installs change the live interpreter. Do this on staging first, or in an approved window.

Debian and Ubuntu (example, replace with the branch you target):

sudo apt-get update
sudo apt-get install --only-upgrade php8.3-cli php8.3-fpm php8.3-mysql php8.3-curl

RHEL, AlmaLinux, Rocky Linux:

sudo dnf upgrade 'php*'

On Ubuntu 24.04 noble, php8.3-* is fixed at 8.3.6-0ubuntu0.24.04.13 (USN-8948-1). Do not replace those packages with an upstream tarball to chase a version number.

Step 5. Rebuild custom extensions {#step-phpize}

Only needed for custom .so files that you must keep. Build on a staging host with the target PHP development package installed.

CONFIG

Debian and Ubuntu (development headers, example for 8.3):

sudo apt-get install php8.3-dev build-essential autoconf

RHEL, AlmaLinux, Rocky Linux:

sudo dnf install php-devel gcc make autoconf

CONFIG

Then, inside your extension source directory:

phpize --clean
phpize
./configure --with-php-config="$(command -v php-config)"
make -j"$(nproc)"
make test

Confirm that php-config belongs to the target branch before you run ./configure. A wrong php-config is the most common cause of an extension that builds fine and fails to load.

READ-ONLY

php-config --version
php-config --phpapi
php-config --extension-dir
Illustrative output8.3.6
20230831
/usr/lib/php/20230831

CAUTION

Installing the built file replaces what is in the live extension directory. Keep a copy of the old .so first:

sudo cp -a /usr/lib/php/20230831/myext.so /root/myext.so.bak
sudo make install

Adjust the directory to the one printed by php-config --extension-dir. The numbers above are an example of the layout, not a value to copy.

Step 6. Enable and test the load {#step-enable}

CONFIG

Debian and Ubuntu:

echo 'extension=myext.so' | sudo tee /etc/php/8.3/mods-available/myext.ini
sudo phpenmod -v 8.3 myext

RHEL, AlmaLinux, Rocky Linux:

echo 'extension=myext.so' | sudo tee /etc/php.d/50-myext.ini

READ-ONLY

Check the load before you touch production workers:

php -m | grep -i myext
php -d display_errors=1 -r 'echo "load ok\n";'

Startup warnings appear on the console. Read them.

SERVICE

Restart the web SAPI so workers load the new extension and the new interpreter. A package install without a restart is incomplete.

Debian and Ubuntu:

sudo systemctl restart php8.3-fpm

RHEL, AlmaLinux, Rocky Linux:

sudo systemctl restart php-fpm

Troubleshooting: Unable to load dynamic library {#troubleshooting-load}

The most frequent failure reads like this on startup.

Illustrative outputPHP Warning: PHP Startup: Unable to load dynamic library 'myext.so' (tried: /usr/lib/php/20230831/myext.so (/usr/lib/php/20230831/myext.so: undefined symbol: ...)) in Unknown on line 0
Message fragment Likely cause Fix
cannot open shared object file: No such file Wrong extension_dir or file not installed Check php-config --extension-dir and where make install put the file.
undefined symbol Built against a different branch’s headers Rebuild with the correct php-config.
Module compiled with module API=... API mismatch between build and interpreter Clean with phpize --clean, rebuild for the target.
Module compiled with build ID=API...,NTS vs ZTS Thread safety flavor mismatch Use a build matching the interpreter flavor.
Unable to load dynamic library for a packaged module Package not installed for the new branch Install the matching php8.x-<name> package.
Loader error from a third-party extension Vendor build does not support the target PHP Get the vendor build, or keep the old branch temporarily and mitigate.
TROUBLESHOOT

If the web SAPI fails to start after an update, run php-fpm8.3 -t (Debian family) or php-fpm -t (RHEL family) and read the first error. Disable only the failing extension ini, test again, and restart. Do not delete the whole conf.d directory.

WARNING

A loader that fails closed takes the site down. A loader that is simply absent is also a failure, because the application cannot decode its files. Have the rollback ready before the window, and test the loader on staging with the real application code.

Cross-branch moves {#cross-branch}

If the target is a different branch, widen the checks.

  1. Read each extension’s own release notes for the target PHP branch.
  2. Rebuild every custom .so. Do not copy files from the old branch.
  3. Check application code for removed functions and changed behavior in the target branch, using staging and your test suite.
  4. Test with the same php.ini settings as production, or the differences will hide failures.
  5. Plan the cutover with a rollback. See minimal-downtime upgrades.
LEGACY

Staying on PHP 8.1 or older because an extension will not build is a temporary position. Ask the vendor for a backport, check whether a minimal source backport exists (7.4, 8.0, and 8.1 only), and set a migration date. Do not assume a 5.6 patch exists.

Completion checklist {#completion-checklist}

  • Module lists saved for CLI and for the web SAPI, and the differences understood.
  • Every extension classified as packaged, custom, or third-party loader.
  • Packaged modules updated together with the interpreter, with matching branch names.
  • Custom .so files rebuilt with phpize against the target php-config.
  • php-config --version and --phpapi confirmed to match the target before make install.
  • Old .so files backed up before replacement.
  • Third-party loaders checked against the vendor’s build for the target PHP.
  • php-fpm -t passes and the startup log has no “Unable to load dynamic library” warnings.
  • Workers restarted, and the running version verified.
  • Rollback tested or at least written down with a named owner.

Questions

Do I need to rebuild extensions for a security update inside the same branch?

Usually not for packaged modules, because they update together with the interpreter. Custom .so files built outside the package manager should be rebuilt or checked against the new headers.

Why does php -m on the CLI not match what the website loads?

The CLI and the web SAPI read different php.ini and conf.d directories. An extension enabled only for FPM or Apache will not show in CLI output.

Sources checked