Crimson.Lab

Лаборатория Кримсона

29 October 2026

PHP 8.3.36 на CentOS 7: ещё один фикс для страждущих

Для тех, кто всё ещё держит CentOS 7, но уже перешёл на PHP 8.3, — ещё один комплект. В предыдущих статьях мы обновляли PHP 8.2 и ставили бэкпорт для PHP 7.4. Теперь пересобрали PHP 8.3.36 для EL7: исправления CVE-2026-109618 и CVE-2026-109619, готовые RPM, исходный пакет, скрипты сборки, установки и отката.

Боевого сервера на PHP 8.3 у нас нет. Типичное окружение воспроизвели в Docker и проверили обновление там, включая цикл «установка → откат → повторная установка». Ниже — что пришлось поменять относительно сборки 8.2 и как применить комплект к своему серверу.

Для какого окружения

Компонент Проверенное окружение
ОС и архитектура CentOS 7.9, x86_64
PHP до обновления 8.3.8-1.el7.remi из remi-php83
PHP после обновления 8.3.36-1.el7.local
Веб-сервер Apache 2.4.6 prefork + mod_php
Дополнительно php-fpm, проверка конфигурации и перезапуска
Системные библиотеки OpenSSL 1.0.2, glibc 2.17, ICU 73
API модулей PHP 20230831 до и после обновления

В контейнере стояли 15 основных пакетов PHP и расширения APCu 5.1.23, redis6 6.0.2, memcached 3.2.0, imagick-im6 3.7.0, zip 1.22.3, igbinary 3.2.15, msgpack 2.2.0 и mcrypt 1.0.7. Этот набор пережил обновление без пересборки PECL.

Комплект рассчитан на уже установленный PHP 8.3 с обычными именами пакетов php, php-cli, php-fpm. Для параллельной установки Remi SCL с именами php83-php-*, другой архитектуры или перехода с PHP 7.4/8.2 эти команды не подходят. Совпадение API модулей также не заменяет проверку приложения и собственных расширений.

В архивном репозитории Remi для PHP 8.3 на EL7 осталась версия 8.3.8. Поэтому обычный yum update задачу не решает: берём новый исходный RPM и пересобираем под старое окружение.

Что именно исправляем

PHP 8.3.36 — официальный релиз безопасности. В списке изменений указаны исправления двух проблем:

Для Apache с mod_php подтверждено удалённое выполнение кода без авторизации. Подробности и дополнительные ограничения на уровне веб-сервера разобраны в статье про PHP2Shell. Здесь исправления уже находятся в исходниках PHP 8.3.36, а наши дополнительные патчи нужны для сборки на старых библиотеках CentOS 7.

Забираем комплект

Всё находится в assets/blog/php83-update:

Материал Где взять
32 бинарных RPM без debuginfo Каталог rpms/x86_64
Контрольные суммы бинарных RPM SHA256SUMS
Исходный пакет для EL7 php-8.3.36-1.el7.local.src.rpm
Контрольная сумма SRPM SHA256SUMS
Скрипты и патчи Каталоги build/, test/ и server/ в том же комплекте

Забираем только нужный каталог репозитория:

git clone --depth 1 --filter=blob:none --sparse \
  https://github.com/william-aqn/blog.git blog-php83-update
git -C blog-php83-update sparse-checkout set assets/blog/php83-update
cd blog-php83-update/assets/blog/php83-update

Дальнейшие локальные команды выполняем отсюда, в Bash. Для сборки и проверки нужен Docker с Linux-контейнерами x86_64. Сервер, каталог установки и образ задаём один раз:

SERVER=root@server.example.com
REMOTE_DIR=/root/php-8.3-cve
BUILD_IMAGE=php83-el7-build
CHECK_URL=http://127.0.0.1/

CHECK_URL должен вести на обслуживаемый вашим Apache адрес. Для стандартного BitrixVM это может быть http://127.0.0.1:8888/; при виртуальных хостах выбираем адрес, который действительно проверяет нужный сайт.

Временные меры до обновления

server/mitigate.sh выключает session.upload_progress.enabled, задаёт ограничения post_max_size и upload_max_filesize, меняет существующий client_max_body_size в основном конфиге nginx. Перед изменениями сохраняет копии файлов, затем проверяет конфигурацию nginx и Apache и выполняет reload сервисов.

ssh "$SERVER" "mkdir -p '$REMOTE_DIR'"
scp -r server "$SERVER:$REMOTE_DIR/"
ssh "$SERVER" "cd '$REMOTE_DIR' && \
  INI=/etc/php.d/zz-cve-mitigation.ini LIMIT_MB=64 bash server/mitigate.sh"

При LIMIT_MB=64 получаем post_max_size=64M, upload_max_filesize=60M и лимит nginx 64m. Это пример: значения выбираем под загрузки пользователей. Скрипт меняет существующую директиву nginx, а не добавляет её во все виртуальные хосты. Фактические ограничения проверяем отдельно.

Имя zz-cve-mitigation.ini помогает загрузить настройки после обычных файлов в /etc/php.d. Переопределения в конфигурации веб-сервера, пулах FPM и приложении тоже нужно учитывать. Вывод php -i из скрипта относится к CLI; проверяем результат именно в веб-PHP и убеждаемся, что старые воркеры больше не обслуживают запросы с прежними настройками.

Временные ограничения не устраняют обе уязвимости. memory_limit=-1 из рекомендаций PHP скрипт не выставляет: отключение лимита без продуманного ограничения ресурсов может привести к системному OOM и остановке других сервисов. Сохранённый лимит тоже не является заменой исправлению — цель этого шага только выиграть время до установки новых пакетов.

Готовые RPM или своя сборка

Для обоих вариантов сначала создаём образ: он нужен и сборке, и контейнерной проверке готовых пакетов.

docker build -t "$BUILD_IMAGE" build/

Вариант 1. Используем готовые пакеты

Проверяем контрольные суммы и копируем RPM туда, где их ожидает тестовый скрипт:

(cd rpms && sha256sum -c SHA256SUMS)
(cd rpms/x86_64 && sha256sum -c SHA256SUMS)
mkdir -p out/RPMS/x86_64
cp rpms/x86_64/*.rpm rpms/x86_64/SHA256SUMS out/RPMS/x86_64/

Не устанавливаем весь каталог через rpm -Uvh *.rpm: среди 32 файлов есть отладчик, заголовки для разработки и дополнительные SAPI. Установщик ниже выбирает подпакеты, которые уже есть на сервере.

Вариант 2. Собираем самостоятельно

Вместо копирования готовых RPM запускаем сборку:

docker run --rm -v "$PWD/out:/out" "$BUILD_IMAGE"

build/build.sh скачивает php-8.3.36-1.remi.src.rpm, распаковывает его, сверяет SHA-256 архива исходников PHP с php.net, адаптирует spec и вызывает rpmbuild.

Результат Путь
Бинарные RPM и контрольные суммы out/RPMS/x86_64/
Исходный пакет с правками для EL7 out/SRPMS/php-8.3.36-1.el7.local.src.rpm
Лог сборки out/build.log

В нашей проверке установка зависимостей и сборка заняли около 11 минут. Собираются NTS и ZTS; регрессионные тесты PHP отключены параметром --without tests. Проверка обновления выполняется отдельно.

Старый RPM из CentOS 7 не устанавливает современный SRPM через rpm -i из-за rpmlib(RichDependencies), поэтому используем rpm2cpio. Проверку подписи этого SRPM он также не выполняет. Сверка архива PHP с php.net подтверждает содержимое самого архива, но не проверяет spec и остальные патчи внутри исходного пакета.

Четыре правки для CentOS 7

Полный diff spec-файла лежит рядом со сборочным скриптом. Две правки знакомы по PHP 8.2, ещё две понадобились для ветки 8.3.

Правка Зачем
ICU: >= 74 → >= 73 Используем доступную в Remi EL7 библиотеку libicu73, с которой работал прежний php-intl
pkgconfig(odbc) → unixODBC-devel У unixODBC 2.3.1 из CentOS 7 нет нужного файла .pc; configure умеет искать библиотеку через каталог /usr
OpenSSL 1.0.2, Patch301 Добавляем совместимость для вызова ASN1_STRING_get0_data() в ext/openssl/xp_ssl.c
glibc 2.17, Patch302 Обходим конфликт макросов strncmp и ZEND_STRL в ext/ffi/ffi.c

OpenSSL: та же правка, что для 8.2

В патче OpenSSL внутри существующего условия для старого API добавлена одна строка:

#define ASN1_STRING_get0_data(_asn1) ((const unsigned char *) ASN1_STRING_data(_asn1))

Так новый код использует доступную функцию OpenSSL 1.0.2. В логе нашей сборки предупреждений implicit declaration of function не было.

FFI: макрос получает два аргумента вместо трёх

На старой glibc при оптимизации strncmp() может оказаться макросом с тремя аргументами. В PHP встречается такой вызов:

strncmp(p, ZEND_STRL("FFI_LIB"))

ZEND_STRL разворачивается в строку и её длину, но для подсчёта аргументов внешнего макроса это происходит слишком поздно. Сборка падает с ошибкой:

macro "strncmp" requires 3 arguments, but only 2 given

Патч FFI меняет три вызова на форму (strncmp)(...). Скобки вокруг имени не дают раскрыть макрос; вызывается обычная функция с теми же аргументами.

Проверяем обновление до установки

test/smoke-test.sh устанавливает старый PHP, расширения и Apache, сохраняет RPM для отката, обновляет основные пакеты и сравнивает список модулей. Дополнительно проверяет конфигурацию FPM и выводит ответ страницы через mod_php.

Параметры по умолчанию соответствуют нашему контейнеру: OLD=8.3.8-1.el7.remi, NEW=8.3.36-1.el7.local и REPO=remi-php83. На сервере сначала смотрим реальные пакеты:

ssh "$SERVER" "rpm -qa 'php*' --qf '%{NAME} %{VERSION}-%{RELEASE} %{SOURCERPM}\n'"

В CORE передаём имена установленных подпакетов из исходного RPM php-8.3.*. В PECL — остальные расширения, желательно с точными версиями. Если набор отличается от примера ниже, меняем массивы; если отличается старый выпуск, передаём тесту свой -e OLD=....

CORE_PACKAGES=(
  php php-bcmath php-cli php-common php-fpm php-gd php-intl php-mbstring
  php-mysqlnd php-opcache php-pdo php-process php-soap php-sodium php-xml
)
PECL_PACKAGES=(
  php-pecl-apcu php-pecl-redis6 php-pecl-memcached php-pecl-imagick-im6
  php-pecl-zip php-pecl-igbinary php-pecl-msgpack php-pecl-mcrypt
)

Сохраняем конфиги и список модулей с сервера. Если FPM не установлен, убираем etc/php-fpm.d из команды и php-fpm из CORE_PACKAGES. Если какой-то из файлов Apache отсутствует, корректируем список под свою установку.

ssh "$SERVER" \
  'tar -C / -czf - etc/php.ini etc/php.d etc/php-fpm.d etc/httpd/conf.d/php.conf etc/httpd/conf.modules.d/20-php.conf' \
  > prod-php-conf.tgz
ssh "$SERVER" 'php -m 2>&1' > prod-php-modules.txt

Архив конфигурации оставляем у себя: это входные данные теста, публиковать их вместе с комплектом не нужно. Запускаем проверку:

docker run --rm \
  -v "$PWD/out:/out" \
  -v "$PWD/test:/test:ro" \
  -v "$PWD/prod-php-conf.tgz:/prod-php-conf.tgz:ro" \
  -v "$PWD/prod-php-modules.txt:/prod-php-modules.txt:ro" \
  -e CORE="${CORE_PACKAGES[*]}" \
  -e PECL="${PECL_PACKAGES[*]}" \
  --entrypoint bash "$BUILD_IMAGE" -c \
  'bash /test/smoke-test.sh && diff /prod-php-modules.txt /tmp/m-before.txt'

Последний diff нужен, чтобы сравнить контейнер с сервером, а не только контейнер до и после обновления. Если точной версии PECL в репозитории уже нет, yum может пропустить пакет. В таком случае копируем соответствующие .so с сервера в server-modules/ и добавляем к запуску -v "$PWD/server-modules:/server-modules:ro". Скрипт умеет подставлять эти файлы и устанавливать недостающие библиотечные зависимости.

В нашем тесте список модулей сохранился, содержимое списка файлов php.d не изменилось, php-fpm -t прошёл успешно, mod_php ответил 8.3.36 apache2handler.

Просматриваем и сам ответ страницы, и журнал Apache: скрипт выводит их, но не каждую ошибку HTTP или PHP превращает в неуспешное завершение. Контейнер также не разворачивает Битрикс с базой данных. Авторизацию, загрузки, обмены, cron и собственные доработки проверяем отдельно на копии сайта.

Устанавливаем с полным перезапуском

До запуска делаем резервную копию сервера и проверяем, что out/rollback/ содержит все старые основные RPM именно нашего набора и выпуска. Они понадобятся, если обновление окажется несовместимым.

server/install.sh выбирает установленные подпакеты из исходного пакета PHP, выполняет сверку по SHA256SUMS, сохраняет сведения о модулях, конфигурации и пакетах в /root/php-update-*. Затем делает rpm -Uvh --test, устанавливает выбранные RPM и сравнивает модули.

Сам установщик не запрещает переход между ветками PHP. Поэтому проверка, что сервер уже использует PHP 8.3, обязательна: имена php-common и php-cli одинаковы у разных веток. PECL-пакеты установщик не обновляет.

Перед перезапуском есть ещё две особенности:

Копируем файлы и запускаем установку в согласованное окно работ:

ssh "$SERVER" "mkdir -p '$REMOTE_DIR'"
scp -r out/RPMS/x86_64 out/rollback server "$SERVER:$REMOTE_DIR/"
ssh "$SERVER" "cd '$REMOTE_DIR' && CHECK_URL='$CHECK_URL' bash server/install.sh x86_64"

Если список модулей изменился, скрипт завершится до своего шага перезапуска. Пакеты на диске к этому моменту уже обновлены: разбираемся с причиной или откатываемся. Это не отмена транзакции RPM.

При успешном сравнении выполняется полный рестарт httpd, затем активных httpd-scale и php-fpm. Для mod_php обычного graceful reload недостаточно: мастер Apache продолжает держать старую библиотеку libphp.so в памяти.

В конце скрипт ищет старые отображённые библиотеки и исполняемые файлы в /proc/*/maps процессов Apache и FPM. Нулевой счётчик старого кода дополняем проверкой состояния сервисов, ответа сайта и журналов: вывод HTTP-кода в установщике сам по себе не гарантирует работоспособность приложения.

Что получилось на стенде

На воспроизведённом окружении с работающими Apache и FPM проверили полный цикл:

Этап Результат
Установка Обновились 15 основных пакетов, список модулей сохранился
Apache после рестарта В баннере PHP/8.3.36, процессов со старым кодом не обнаружено
FPM после рестарта Новый мастер использует /usr/sbin/php-fpm 8.3.36 без отметки (deleted)
Откат Вернулся PHP 8.3.8, оба сервиса перезапущены
Повторная установка Снова PHP 8.3.36

Это результаты контейнерной проверки комплекта. Из них нельзя сделать вывод о времени простоя или отсутствии 502/504 на нагруженном сайте: в случае PHP 7.4 даже короткий рестарт затронул пользователей.

Как откатить

server/rollback.sh выбирает старые RPM для установленных пакетов, выполняет rpm -Uvh --oldpackage и полностью перезапускает Apache и активные дополнительные сервисы:

ssh "$SERVER" "bash '$REMOTE_DIR/server/rollback.sh' '$REMOTE_DIR/rollback'"

После отката снова проверяем сайт, версию PHP через веб-SAPI и журналы. Возврат к 8.3.8 возвращает и уязвимости, поэтому это средство восстановления работоспособности на время устранения проблемы с обновлением.

Для будущего релиза номер версии можно передать сборщику через PHP_VERSION. Это потребует заново проверить исходный SRPM, применение обоих патчей и соответствие NEW в smoke-тесте. Текущий комплект и готовые RPM проверены для 8.3.36.

Обновление PHP закрывает эти две CVE, но не возвращает поддержку CentOS 7 и его системным библиотекам. Комплект помогает пережить срочное исправление; перенос сайта на поддерживаемое окружение остаётся отдельной задачей.

tags: php - bitrix - centos - security - docker