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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions