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 — официальный релиз безопасности. В списке изменений указаны исправления двух проблем:
- CVE-2026-109618 / GHSA-w8hw-w9fx-j79w: повреждение состояния при аварийном выходе из-за нехватки памяти.
- CVE-2026-109619 / GHSA-6wmf-4rv5-p3rp: обращения к состоянию предыдущего запроса после освобождения его памяти.
Для 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-пакеты установщик не обновляет.
Перед перезапуском есть ещё две особенности:
- При
PrivateTmp=yesуhttpd.serviceскрипт останавливается. Нужно проверитьsession.save_path: файловые сессии в приватном/tmpмогут исчезнуть при полном рестарте.FORCE=1предусмотрен для осознанного обхода этой проверки. STUCK_AGEпо умолчанию равен 3600 секундам. Скрипт принудительно завершает дочерние процессы старше этого порога. Возраст воркера не равен длительности запроса и не доказывает зависание; такой процесс может быть исправен, а его текущий запрос будет оборван. До запуска оцениваем этот механизм и порог для своего сервера.
Копируем файлы и запускаем установку в согласованное окно работ:
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