PHP 7.4 на CentOS 7: бэкпорт для CVE-2026-109618 и CVE-2026-109619
В первой части мы пересобрали PHP 8.2.35 для старого BitrixVM на CentOS 7. Следом нашёлся второй сервер: такой же CentOS 7, но Битрикс 22.x и PHP 7.4. Уязвимости те же — CVE-2026-109618 и CVE-2026-109619, а нового официального релиза PHP для этой ветки нет.
Здесь пришлось устанавливать бэкпорт — исправления, перенесённые в старую версию. Собрали 7.4.33-31.el7.local, сохранили совместимость с установленными расширениями и обновили сервер. По дороге обнаружились ошибка проверки ICU, исчезнувшая из репозитория версия zip и короткий рестарт, который всё-таки успел дать пользователям 502/504.
Архивные репозитории CentOS и EPEL, распаковку SRPM через rpm2cpio и необходимость полного рестарта Apache уже разобрали в первой части. Здесь — отличия PHP 7.4. Комплект для этой статьи самостоятельный: свои скрипты, патч и готовые RPM.
Что стояло на втором сервере
| Компонент | Окружение |
|---|---|
| ОС и окружение | CentOS 7.9, BitrixVM 7.5 |
| Веб-сервер | nginx 1.20 → Apache 2.4.6 prefork + mod_php |
| Панель BitrixVM | Отдельный httpd-scale |
| PHP | 7.4.33-15.el7.remi, 15 подпакетов, включая отдельный php-json |
| Расширения | APCu 5.1.22, geoip 1.1.1, mcrypt 1.0.6, memcache 4.0.5.2, rrd 2.0.3, Xdebug 2.9.8, zip 1.21.1 |
| OpenSSL | Системный 1.0.2k |
| Битрикс | 22.x |
Откуда взялось исправление для неподдерживаемой ветки
Поддержка PHP 7.4 закончилась 28 ноября 2022 года. В октябре 2026-го команда PHP предоставила минимальный бэкпорт для этой ветки: о нём говорится в официальном сообщении об уязвимостях, исходники находятся в ветке PHP-7.4-CVE-2026-109618-109619.
Это исправление исходников, а не новый релиз PHP 7.4. Remi включил его в php-7.4.33-31.remi.src.rpm отдельным патчем php-7.4-cve-2026-109618.patch. В changelog пакета от 21 октября указаны обе CVE и изменение Avoid state corruption via bailout.
Переход выглядит так:
| Что проверяем | До обновления | После обновления |
|---|---|---|
| Версия PHP | 7.4.33 | 7.4.33 |
| Версия и выпуск RPM | 7.4.33-15.el7.remi |
7.4.33-31.el7.local |
По одному php -v наличие бэкпорта не определить. Номер PHP остаётся прежним; смотреть нужно также выпуск RPM и его changelog. Вместе с переходом от -15 к -31 приходят накопленные исправления Remi за 2024–2026 годы.
Что изменилось в поведении PHP
Самое заметное изменение — session upload progress отключён в коде. В обработчике сессий условие стало таким:
if (!PS(rfc1867_enabled) || true) {
return retval;
}
Теперь данные о ходе загрузки файла не появляются в $_SESSION, даже если session.upload_progress.enabled = On. В нашей установке Битрикса эта возможность не использовалась. Если на ней построен собственный индикатор загрузки, его поведение изменится. Сама загрузка файлов при этом остаётся доступной.
Есть ещё два изменения: при разборе multipart сохраняются только заголовки Content-Disposition и Content-Type каждой части, а обработка исчерпания памяти даёт другой вывод, чем раньше. Это проявилось в тестах, о которых ниже.
Пересобирать установленные PECL-расширения в нашем случае не понадобилось. Бэкпорт дополняет API движка, но не меняет раскладку структур в затронутых заголовках. Проверили все семь расширений сервера: шесть — в точных версиях из репозитория, а zip.so — непосредственно с боевого сервера.
Забираем комплект
На машине сборки нужны Bash и Docker с Linux-контейнерами x86_64. Получаем каталог с материалами:
git clone --depth 1 --filter=blob:none --sparse \
https://github.com/william-aqn/blog.git blog-php7-update
git -C blog-php7-update sparse-checkout set assets/blog/php7-update
cd blog-php7-update/assets/blog/php7-update
Все дальнейшие локальные команды выполняем из этого каталога. Адрес сервера и каталог установки задаём один раз:
SERVER=root@server.example.com
REMOTE_DIR=/root/php-7.4-cve
Временные меры до установки
server/mitigate.sh выполняет те же действия, что и в первой части: выключает session.upload_progress, выставляет post_max_size=64M, upload_max_filesize=60M и меняет существующий лимит тела запроса в основном конфиге nginx на 64m. Значения подбираем под реальные загрузки пользователей.
ssh "$SERVER" "mkdir -p '$REMOTE_DIR'"
scp -r server "$SERVER:$REMOTE_DIR/"
ssh "$SERVER" "cd '$REMOTE_DIR' && LIMIT_MB=64 bash server/mitigate.sh"
Скрипт сохраняет копии конфигов, проверяет их и выполняет reload. На этом этапе в просмотренных логах второго сервера 5xx не появилось. Как и в первой части, это только временные ограничения: они не заменяют установку исправлений, а новые настройки должны примениться ко всем рабочим процессам.
Собираем RPM
Dockerfile использует CentOS 7 и архивные репозитории. build.sh скачивает SRPM, ищет CVE-2026-109618 в changelog, проверяет наличие файла бэкпорта и сверяет SHA-256 архива php-7.4.33.tar.xz с php.net. Затем адаптирует spec и запускает сборку.
Сверка архива PHP не распространяется на остальные файлы SRPM, включая spec и дополнительные патчи. Особенности проверки подписи старым RPM остаются теми же, что в первой части.
docker build -t php74-el7-build build/
docker run --rm -v "$PWD/out:/out" php74-el7-build
Результаты появятся в out/RPMS/x86_64/, out/SRPMS/ и out/build.log. На 8 vCPU установка зависимостей заняла около четырёх минут, сборка с тестами — около шести.
Ловушка с ICU и C++17
Для php-intl сначала снижаем требование ICU с 74 до 73 — такая библиотека доступна в используемом Remi EL7. Но одной правки BuildRequires оказалось мало.
В патче Remi для поддержки ICU 74 проверка версии была записана так:
AS_IF([test "$PKG_CONFIG icu-uc --atleast-version=74"],[
Это проверка непустой строки, а не запуск pkg-config. Она всегда выбирает ветку с требованием C++17. Системный GCC 4.8 его не поддерживает, и при сборке ext/intl получаем ошибки в заголовках ICU:
'char16_t' does not name a type
Исправление — выполнить команду проверки версии:
- AS_IF([test "$PKG_CONFIG icu-uc --atleast-version=74"],[
+ AS_IF([$PKG_CONFIG icu-uc --atleast-version=74],[
С ICU 73 после этого выбирается ветка C++11. Правка находится в php-7.4.33-icu-check.patch и подключается как Patch304. Все изменения spec собраны в php74.spec.diff.
А вот патч для OpenSSL 1.0.2 из первой статьи здесь не понадобился: xp_ssl.c в PHP 7.4 не вызывает проблемную ASN1_STRING_get0_data(), а в openssl.c есть замена для старого API. Проверка используемых символов OpenSSL прошла; предупреждений implicit declaration of function в логе нашей сборки не было.
Что показали тесты PHP
В отличие от сборки 8.2, здесь набор тестов PHP запускается по умолчанию. В нашем прогоне получились такие результаты:
| Результат | Количество |
|---|---|
| Пройдено | 9 071 |
| Пропущено | 6 788 |
| Ожидаемые падения, отмеченные в тестах | 33 |
| Падения без такой отметки | 9 |
| Всего | 15 901 |
Из девяти падений восемь приходятся на ext/session/tests/rfc1867*.phpt: они ожидают данные upload progress, который бэкпорт отключает. Ещё один, tests/lang/bug45392.phpt, ожидает прежний вывод при нехватке памяти. По нашему разбору эти результаты связаны с изменениями бэкпорта; обычные тесты загрузки файлов прошли.
Падения тестов не останавливают эту RPM-сборку. Успешного завершения docker run недостаточно: нужно просмотреть out/build.log и причины ошибок. Пропустить тесты можно через RUN_TESTS=0; скрипт передаёт в spec макрос runselftest 0. Флаг --without tests, использованный в сборке 8.2, здесь ими не управляет.
Проверяем старые расширения
test/smoke-test.sh настроен на переход 7.4.33-15.el7.remi → 7.4.33-31.el7.local из репозитория remi-php74. В списке основных пакетов есть php-json, а конфиг модуля Apache называется 15-php.conf, не 20-php.conf.
Снимаем конфиги и список модулей с сервера:
ssh "$SERVER" \
'tar -C / -czf - etc/php.ini etc/php.d etc/httpd/conf.d/php.conf etc/httpd/conf.modules.d/15-php.conf' \
> prod-php-conf.tgz
ssh "$SERVER" 'php -m' > prod-php-modules.txt
Здесь обнаружилась ещё одна ловушка: в нашем сценарии yum пропускал запрошенные версии пакетов, которых уже не было в репозитории. Так не устанавливались php-pecl-zip-1.21.1 и нужный выпуск php-pear. Простое сравнение модулей до и после обновления внутри контейнера могло бы показать успех: zip отсутствовал бы в обоих списках.
Поэтому к запуску теста ниже добавлено сравнение исходного набора контейнера с серверным списком из prod-php-modules.txt. Сам скрипт такого сравнения не делает. Для проверки старого zip копируем бинарный модуль с сервера:
mkdir -p server-modules
scp "$SERVER:/usr/lib64/php/modules/zip.so" server-modules/
Скрипт помещает файлы из server-modules/ в /usr/lib64/php/modules контейнера и устанавливает недостающие библиотеки. Для нашего zip понадобилась libzip.so.5. Это проверка конкретного расширения; недостающие пакеты PEAR таким способом не восстанавливаются.
Ниже наш набор пакетов. Для своего сервера списки нужно сверить с установленными RPM:
CORE_PACKAGES=(
php php-bcmath php-cli php-common php-gd php-json php-ldap php-mbstring
php-mysqlnd php-opcache php-pdo php-process php-pspell php-sodium php-xml
)
PECL_PACKAGES=(
php-pecl-apcu-5.1.22-1.el7.remi.7.4
php-pecl-geoip-1.1.1-11.el7.remi.7.4
php-pecl-mcrypt-1.0.6-1.el7.remi.7.4
php-pecl-memcache-4.0.5.2-1.el7.remi.7.4
php-pecl-rrd-2.0.3-1.el7.remi.7.4
php-pecl-xdebug-2.9.8-1.el7.remi.7.4
php-zipstream-0.2.2-6.el7
)
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" \
-v "$PWD/server-modules:/server-modules:ro" \
-e CORE="${CORE_PACKAGES[*]}" \
-e PECL="${PECL_PACKAGES[*]}" \
--entrypoint bash php74-el7-build -c \
'bash /test/smoke-test.sh && diff /prod-php-modules.txt /tmp/m-before.txt'
Список модулей до и после обновления у нас совпал. mod_php ответил 7.4.33 apache2handler, а скопированный zip.so версии 1.21.1 создал архив через ZipArchive под Apache. Старые основные пакеты для отката скрипт сохранил в out/rollback/.
Нужно просмотреть ответ страницы, результат проверки zip и журнал Apache: скрипт выводит их, но не считает любую ошибку HTTP или PHP причиной завершиться неуспешно. Как и в первой части, этот контейнер проверяет окружение PHP; работу Битрикса с базой данных и доработками проверяем отдельно.
Устанавливаем и проверяем загруженную библиотеку
Перед установкой готовим резервную копию сервера и проверяем наличие старых RPM в out/rollback/. server/install.sh сохраняет снимок состояния, выполняет тестовую транзакцию RPM, обновляет установленные подпакеты и сравнивает модули. После этого полностью перезапускает Apache и активные дополнительные сервисы.
Проверки PrivateTmp, файловых сессий и настройки CHECK_URL остаются теми же, что в первой части. А вот поиск старого кода в памяти пришлось исправить: у PHP 7.4 модуль называется libphp7.so. Выражение для одного libphp.so такую библиотеку просто не замечало бы.
Есть и особенность завершения процессов. Установщик по-прежнему использует только возраст воркера и порог STUCK_AGE, по умолчанию 3600 секунд. Перед запуском нужно проверить этот порог для своего сервера: возраст процесса не равен времени обработки запроса, а принудительное завершение оборвёт его текущую работу.
Мы сначала выбрали порог «старше 25 минут» и получили почти все 260 процессов: они появились после graceful-перезагрузки 37 минут назад. Ожидание свободного prefork-воркера в semtimedop само по себе не означало зависание. Дополнительно смотрели на процессы, пережившие предыдущую перезагрузку, и их работу; на втором сервере зависших воркеров не обнаружили.
Копируем комплект и устанавливаем пакеты:
ssh "$SERVER" "mkdir -p '$REMOTE_DIR'"
scp -r out/RPMS/x86_64 out/rollback server "$SERVER:$REMOTE_DIR/"
ssh "$SERVER" "cd '$REMOTE_DIR' && bash server/install.sh x86_64"
На сервере проверяем отсутствие старой библиотеки с учётом её имени:
grep -lE 'libphp[0-9]*\.so(;| \(deleted\))' /proc/[0-9]*/maps
При достаточных правах и отсутствии ошибок чтения совпадений быть не должно. Затем проверяем версию установленных пакетов:
ssh "$SERVER" 'rpm -q php php-common php-cli'
Ожидаемый выпуск — 7.4.33-31.el7.local. Проверку RPM дополняем запросом через веб-SAPI и просмотром журналов: файл на диске и код в памяти Apache должны соответствовать новой сборке.
Короткий рестарт всё равно дал ошибки
Установщик отработал за 11 секунд. Обновились 15 пакетов, список модулей сохранился. Сам перезапуск основного httpd занял около секунды, затем перезапустился httpd-scale. Старого кода в памяти не осталось, проверенные виртуальные хосты отвечали 200/302, PHP Fatal в просмотренных логах не было.
Но повторить результат первого сервера без 502 не получилось:
| Период по журналу nginx | Запросов | Ответов 5xx |
|---|---|---|
| До рестарта | 29 548 | 0 |
| Окно рестарта, около 6 секунд | 102 | 23 — 502/504 |
| После рестарта | 1 459 | 0 |
Одна секунда выполнения команды рестарта не означает одну секунду пользовательских последствий. На старте в Apache также один раз появился AH00161: server reached MaxRequestWorkers: за время перезапуска успели накопиться запросы. Поэтому для такого обновления закладываем короткое окно работ и выбираем время с меньшей нагрузкой.
Откат и готовые пакеты
server/rollback.sh ставит сохранённые RPM через rpm -Uvh --oldpackage и полностью перезапускает Apache и активные дополнительные сервисы:
ssh "$SERVER" "bash '$REMOTE_DIR/server/rollback.sh' '$REMOTE_DIR/rollback'"
Используем пакеты 7.4.33-15.el7.remi, сохранённые smoke-тестом. Откат возвращает старый набор исправлений, поэтому после восстановления работоспособности нужно разобраться с причиной несовместимости и повторить обновление.
Для этой статьи доступны 34 готовых RPM для x86_64 без debuginfo и их SHA256SUMS. Это та же сборка, из которой на сервер установили 15 пакетов; их контрольные суммы совпали.
Рядом лежат исходный RPM для EL7 и его SHA256SUMS. Для smoke-теста готовых пакетов нужно поместить содержимое rpms/x86_64/ в ожидаемый скриптом каталог out/RPMS/x86_64/.
Номер выпуска Remi в сборочном скрипте задаётся через REMI_RELEASE. При переходе на другой выпуск потребуется проверить бэкпорт, применимость Patch304 и результаты тестов, а также согласовать параметр NEW у smoke-теста с новой сборкой.
Бэкпорт позволил обновить PHP на этом сервере без немедленного перехода на PHP 8. Поддержку PHP 7.4 и CentOS 7 он не возвращает. Следующий шаг — перенос на поддерживаемое окружение с проверкой совместимости Битрикса, доработок и расширений с новой веткой PHP.
tags: php - bitrix - centos - security - docker