Operating Systems Bug Report

A routine dnf update broke our PHP imagick extension: rolling back with dnf history and versionlock

After a Tuesday dnf update, image uploads on our Rocky Linux PHP servers returned 500s. A repository update moved ImageMagick to a new library version, and our hand-built imagick extension still linked to the old one. How we found it in dnf history, rolled back, pinned versions and made updates boring again.

Kenji Watanabe Kenji Watanabe @kenjiwatanabe · January 30, 2026 · 7 min read · 27 Views
Banner: a routine dnf update broke a PHP imagick extension, rolling back with dnf history and versionlock

We patch our Rocky Linux application servers every Tuesday with a scripted dnf -y update, one server at a time behind the load balancer. This time the first server came back from the update looking healthy: the home page loaded and the health check passed. Within a few minutes, error tracking showed a spike of 500 errors on one endpoint, profile photo upload.

The PHP-FPM log had the startup warning that explained it:

[30-Jan-2026 06:12:41] NOTICE: PHP message: PHP Warning:  PHP Startup: Unable to load dynamic library
'imagick.so' (tried: /usr/lib64/php/modules/imagick.so (libMagickWand-7.Q16HDRI.so.9: cannot open
shared object file: No such file or directory), ...) in Unknown on line 0
[30-Jan-2026 06:14:07] NOTICE: PHP message: PHP Fatal error:  Uncaught Error: Class "Imagick" not found
in /srv/app/app/Services/AvatarResizer.php:22

PHP itself was fine. The imagick extension could not load because a shared library it was linked against no longer existed.

What changed?

dnf history records every transaction with what was installed, upgraded and removed:

sudo dnf history list --reverse | tail -3
ID     | Command line             | Date and time    | Action(s)      | Altered
-------------------------------------------------------------------------------
    41 | update -y                | 2026-01-13 06:04 | Upgrade        |   18
    42 | update -y                | 2026-01-30 06:10 | I, U           |   27
sudo dnf history info 42 | grep -i magick
    Upgrade  ImageMagick7-libs-7.1.1.43-1.el9.remi.x86_64   @remi
    Upgraded ImageMagick7-libs-7.1.1.38-1.el9.remi.x86_64   @@System

And the library the extension wanted versus what was now installed:

ldd /usr/lib64/php/modules/imagick.so | grep -i magick
#   libMagickWand-7.Q16HDRI.so.9 => not found
#   libMagickCore-7.Q16HDRI.so.9 => not found
ls /usr/lib64/libMagickWand-7*
#   /usr/lib64/libMagickWand-7.Q16HDRI.so.10  /usr/lib64/libMagickWand-7.Q16HDRI.so.10.0.2

The new ImageMagick build shipped .so.10; our extension was linked to .so.9.

Where did imagick.so come from?

rpm -qf /usr/lib64/php/modules/imagick.so
# file /usr/lib64/php/modules/imagick.so is not owned by any package

It had been built years earlier with pecl install imagick during server provisioning. The package manager did not know it existed, so nothing stopped ImageMagick from changing underneath it.

What we ruled out

  • Application code. No deploy had happened; the same release worked on the other servers, which had not been updated yet.
  • A PHP version change. PHP packages were updated only to a new patch release within the same stream, which keeps the extension API stable. The failure was in a non-PHP library.
  • SELinux. No AVC denials in ausearch -m avc -ts recent; the error was a missing file.

Root cause

A PHP extension compiled by hand against ImageMagick's shared libraries depended on a specific library soname. A repository update replaced the ImageMagick libraries with a build that bumped the soname, removing the old file. Because the extension was outside the package manager, dnf had no dependency to protect, and our health check never exercised image processing.

Why a library update broke an extension

Shared libraries carry a soname, such as libMagickWand-7.Q16HDRI.so.9. When a library changes its binary interface incompatibly, maintainers bump the number. Programs and extensions linked against the old soname look for exactly that file. Packages built by the distribution or repository are rebuilt and released together, so the package manager either updates them in step or refuses the update. A binary built by hand has no such relationship.

What dnf records and can undo

dnf keeps a history database of transactions. dnf history undo <id> performs the reverse of a transaction: it downgrades what was upgraded, removes what was installed and reinstalls what was removed. It needs the old package versions to be available in an enabled repository or the local cache, which is often not true for repositories that keep only the latest builds.

Environment

Component Version / value
OS Rocky Linux 9
Package manager dnf 4.14
PHP 8.2 module stream, php-fpm from AppStream
ImageMagick ImageMagick7-libs from the Remi repository
Extension imagick built with pecl install, not packaged
Update process Weekly dnf -y update, one server at a time
Health check Static file, not a PHP page

Step 1: take the server out and roll back

We drained the server from the load balancer, then tried to undo only the ImageMagick part. A full dnf history undo 42 would also have rolled back unrelated security updates, so we downgraded the one package instead:

sudo dnf downgrade ImageMagick7-libs-7.1.1.38-1.el9.remi -y
Error: Package ImageMagick7-libs-7.1.1.38-1.el9.remi.x86_64 not available.

The repository no longer carried the old build. Our internal mirror, which snapshots repositories weekly, still had it:

sudo dnf --repofrompath=snap,https://mirror.internal.example.com/remi/enterprise/9/remi/x86_64/2026-01-13/ \
         --repo=snap downgrade ImageMagick7-libs-7.1.1.38-1.el9.remi -y
sudo systemctl restart php-fpm
php -m | grep imagick
# imagick

Uploads worked again. The other servers were held back from updating.

Step 2: pin until the extension is rebuilt

The versionlock plugin stops dnf from changing pinned packages:

sudo dnf install -y 'dnf-command(versionlock)'
sudo dnf versionlock add ImageMagick7 ImageMagick7-libs
sudo dnf versionlock list

Routine updates continued for everything else, including kernel and OpenSSL security updates.

Step 3: install the extension from a repository

The long-term fix was to stop building the extension by hand. The same repository that provides ImageMagick 7 also packages the extension built against it, rebuilt whenever the library changes; alternatively an internal RPM could declare the dependency. We chose the repository package:

sudo pecl uninstall imagick
sudo dnf install -y php-pecl-imagick-im7
rpm -q --requires php-pecl-imagick-im7 | grep -i magick
# libMagickWand-7.Q16HDRI.so.10()(64bit) ...

Now the extension and the library are a dependency pair dnf understands. A future soname bump either updates both or reports a conflict during the update, instead of breaking production. With the package in place we removed the versionlock.

Step 4: change how updates roll out

  • Updates run first on a staging server built from the same image, with a smoke test that uploads and resizes an image.
  • The load balancer health check now hits a PHP endpoint that checks required extensions with extension_loaded(), so a broken stack leaves rotation automatically.
  • Production updates come from a dated repository snapshot that staging has already tested.

Alternatives and trade-offs

  • excludepkgs=ImageMagick7* in dnf.conf. Simple, but easy to forget, and it silently blocks security fixes for ImageMagick, which has had many.
  • Containers with PHP, extensions and libraries in one image, rebuilt and tested as a unit. We moved this application to containers later in the year.

How to test the fix

sudo dnf update --assumeno 2>&1 | grep -i magick     # proposed changes include the extension too
php -r 'echo (new Imagick())->getVersion()["versionString"], PHP_EOL;'
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/health/php    # 200

Confirming in production

All four servers ran the packaged extension a day later. The following Tuesday's update moved ImageMagick and php-pecl-imagick-im7 together on staging, the upload smoke test passed, and the rollout completed without incident.

Regression guard

  • The PHP-aware health check removes any server whose required extensions fail to load within 30 seconds.
  • A weekly job lists files under /usr/lib64 not owned by any package (rpm -qf fails), so hand-installed binaries are found before they break.
  • Updates come from tested snapshots.

Security implications

ImageMagick processes untrusted user uploads and has a long history of security fixes, so pinning it is a real risk. We limited the pin to two days, tracked it in a ticket, and checked the upstream changelog for the skipped release. The ImageMagick security policy file restricting coders to the formats we need was kept in configuration management, so the rollback did not loosen it.

Prevention checklist

  • Health checks should exercise the runtime and its required extensions, not a static file.
  • Install every binary through the package manager so dependencies are tracked.
  • Keep repository snapshots so rollbacks have old packages available.
  • Test updates on staging from the same snapshot before production.
  • Use dnf history info and ldd to see exactly what changed before guessing.

References

Add a label

#php #rhel #dnf #rocky-linux #package-management
Written by
Kenji Watanabe @kenjiwatanabe

I started as a Linux administrator in a hosting company and never really left the command line. Today I look after fleets of Linux servers, edge networking and CDN setups for web platforms, and I still do on-call shifts. My posts are the notes I wish I had found at three in the morning: the symptom, the commands that narrowed it down, the actual cause and the change that fixed it. I try to explain what each number in vmstat, iostat or ss output means, so you can reason about your own system instead of copying commands.

Advertisement

Comments

0

No comments yet. Be the first to comment!

Sign In to post a comment.