Skip to content

Segfault в phpcades при VerifyCades зависит от длины пути к PHP-скрипту #11

@anktx

Description

@anktx

Описание

Расширение phpcades (PHP-обёртка над КриптоПро CSP) крашит процесс (segfault, exit code 139) при вызове CPSignedData::VerifyCades(). Краш воспроизводится стабильно и детерминированно в зависимости от длины полного пути к PHP-скрипту.

Минимальное воспроизведение

<?php
// verify-test.php

$payloadBytes = file_get_contents('/path/to/payload.bin');
$signBytes    = file_get_contents('/path/to/signature.p7s');
$payloadBase64 = base64_encode($payloadBytes);
$signBase64    = base64_encode($signBytes);

$sd = new CPSignedData();
$sd->set_ContentEncoding(BASE64_TO_BINARY);
$sd->set_Content($payloadBase64);
$sd->VerifyCades($signBase64, CADES_BES, 1); // segfault
# Копируем один и тот же файл под короткое и длинное имя:
cp verify-test.php /app/tools/a.php
cp verify-test.php /app/tools/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.php

# Короткий путь → segfault:
php -d opcache.enable=0 /app/tools/a.php

# Длинный путь → OK:
php -d opcache.enable=0 /app/tools/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.php

Условие

Segfault возникает, если полный путь к скрипту короче 40 символов.

Проводился бинарный поиск — один и тот же файл копировался под разными именами:

Полный путь Длина Результат
/app/tools/a.php 16 segfault
/app/tools/aaaaaaaaaaaaaaaaaaaa.php 36 segfault
/app/tools/aaaaaaaaaaaaaaaaaaaaaaa.php 39 segfault
/app/tools/aaaaaaaaaaaaaaaaaaaaaaaa.php 40 OK
/app/tools/aaaaaaaaaaaaaaaaaaaaaaaaa.php 41 OK
/app/tools/SOLUTION_verify_detached_cades.php 45 OK

Порог: 40 символов полного пути.

Идентичные файлы — разный результат

Побайтовое сравнение файлов подтвердило идентичность (md5 совпадает). При этом:

  • SOLUTION_verify_detached_cades.php (45 символов) — работает стабильно 3/3 запусков
  • test_step1.php (30 символов) — падает стабильно 3/3 запусков

Переименование подтверждает, что результат определяется путём, а не содержимым:

# Файл A (длинное имя) работает, файл B (короткое имя) падает
cp verify-test.php /app/tools/SOLUTION_verify_detached_cades.php  # OK
cp verify-test.php /app/tools/test_step1.php                       # segfault

# Меняем имена местами — результат меняется вслед за именем
mv /app/tools/SOLUTION_verify_detached_cades.php /app/tools/backup.php
mv /app/tools/test_step1.php /app/tools/SOLUTION_verify_detached_cades.php

php /app/tools/SOLUTION_verify_detached_cades.php  # OK (бывший test_step1)
php /app/tools/backup.php                           # segfault (бывший SOLUTION)

Способ вызова (напрямую / через sh -c) на результат не влияет — только длина пути.

Окружение

Компонент Версия
КриптоПро CSP 5.0 (linux-amd64_deb)
PHP 8.4.20 (cli, NTS)
phpcades commit 9ec6829 (master)
ОС контейнера Debian bookworm (php:8.4-cli-bookworm)
OPcache отключён (opcache.enable=0)

Установленная причина: сырой указатель на zend_string в set_Content

Анализ исходного кода phpcades

Анализ репозитория https://github.com/CryptoPro/phpcades выявил корневую причину.

Файл: src/PHPCadesCPSignedData.cpp, строки ~196-210:

PHP_METHOD(CPSignedData, set_Content) {
    char *sVal;
    size_t lVal;

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "s", &sVal, &lVal) == FAILURE)
        RETURN_WITH_EXCEPTION(E_INVALIDARG);

    HR_ERRORCHECK_RETURN(obj->m_pCppCadesImpl->put_Content(sVal, lVal));
}

sVal — это прямой указатель во внутренний буфер zend_string (поле val[1]). Он передаётся без копирования в закрытую библиотеку libcppcades.so через put_Content(). Если библиотека хранит этот указатель или читает за границей lVal байт при последующем вызове VerifyCades(), происходит чтение из кучи, layout которой зависит от пути к скрипту.

phpcades — C++ расширение, работающее с PHP-строками через структуру zend_string:

struct _zend_string {
    zend_refcounted gc;    // 8 байт
    zend_ulong        h;   // 8 байт
    size_t           len;  // 8 байт
    char             val[1]; // тело строки
};

Полная аллокация: 24 + длина_строки + 1 байт.

Аллокатор glibc malloc выделяет память бинами фиксированного размера: 32, 48, 64, 80, 96...

Полный путь Длина пути Аллокация zend_string Бин malloc Padding
/app/tools/a.php 16 41 48 7
.../aaaaaaaaaaaaaaaaaaaaaaa.php 39 64 64 0
.../aaaaaaaaaaaaaaaaaaaaaaaa.php 40 65 80 15

Когда zend_string занимает бин полностью (padding = 0), чтение за границей попадает на метаданные следующего блока кучи → segfault. Когда padding > 0 — читает нулевые байты заполнения → не падает.

Порог 40 символов — это граница между бинами 64 и 80 аллокатора glibc.

Рекомендуемый фикс

// В set_Content — делать копию с null-терминатором:
std::vector<char> contentCopy(sVal, sVal + lVal + 1);
contentCopy[lVal] = '\0';
HR_ERRORCHECK_RETURN(obj->m_pCppCadesImpl->put_Content(contentCopy.data(), lVal));

Workaround

Создание трёх объектов CPSignedData с предварительными «холостыми» вызовами VerifyCades (trio-паттерн) устраняет segfault, так как меняет layout кучи. Однако это workaround, а не исправление корневой причины.

<?php
// Workaround: trio-паттерн

$payloadBase64 = base64_encode(file_get_contents('/path/to/payload.bin'));
$signBase64    = base64_encode(file_get_contents('/path/to/signature.p7s'));

// Dummy #1 — безопасно упадёт с 0x80070057
$sd1 = new CPSignedData();
$sd1->set_ContentEncoding(BASE64_TO_BINARY);
$sd1->set_Content($payloadBase64);
try { $sd1->VerifyCades($signBase64, CADES_BES, true); } catch (Throwable $e) {}

// Dummy #2 — безопасно упадёт с 0x80070057
$sd2 = new CPSignedData();
$sd2->set_ContentEncoding(ENCODE_BINARY);
$sd2->set_Content(base64_decode($payloadBase64));
try { $sd2->VerifyCades($signBase64, CADES_BES, true); } catch (Throwable $e) {}

// Рабочий вызов
$sd = new CPSignedData();
$sd->set_ContentEncoding(BASE64_TO_BINARY);
$sd->set_Content($payloadBase64);
try {
    $sd->VerifyCades($signBase64, CADES_BES, 1);
} catch (Throwable $e) {
    // 0x800B010E — подпись верна, проверка отзыва недоступна
}

$signer = $sd->get_Signers()->get_Item(1);
$cert   = $signer->get_Certificate();
echo $cert->GetInfo(CERT_INFO_SUBJECT_SIMPLE_NAME) . "\n";

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions