Отладка памяти

Эта глава представляет собой краткое введение в отладку памяти для исходного кода PHP. Это не полный курс: отладка памяти не сложна, но для неё нужен некоторый опыт, который нарабатывается практикой — и его вам, вероятно, придётся нарабатывать в любом случае при разработке любого кода на C. Здесь мы представим очень известный отладчик памяти: valgrind; и расскажем, как использовать его с PHP для отладки проблем с памятью.

Небольшая заметка о valgrind

Valgrind — это хорошо известный инструмент, используемый в разных Unix-окружениях для отладки множества распространённых сценариев проблем с памятью в любом программном обеспечении, написанном на C/C++. Valgrind — это многофункциональный фронтенд для отладки памяти. Наиболее часто используемый базовый инструмент называется «memcheck». Он работает, заменяя каждую кучу-аллокацию libc своей собственной и отслеживая, что вы с ней делаете. Также может быть интересно использование «massif»: это трекер памяти, который может быть полезен для понимания общего использования памяти кучи программой.

Note

Чтобы углубиться в тему, стоит почитать документацию Valgrind. Она хорошо написана, с небольшими показательными примерами.

Чтобы замена аллокации памяти произошла, вам нужно запустить программу, которую вы хотите проанализировать (в нашем случае PHP), через valgrind, то есть запускаемым бинарником будет именно valgrind.

Поскольку valgrind заменяет и отслеживает все аллокации кучи libc, он сильно замедляет отлаживаемые программы. Вы заметите это и в случае PHP. Хотя замедление в случае PHP не настолько драматично, его всё же можно явно почувствовать; не беспокойтесь, если заметите это — это нормально.

Valgrind — не единственный инструмент, который можно использовать, но самый распространённый. Dr.Memory, LeakSanitizer, Electric Fence, AddressSanitizer — другие распространённые инструменты.

Перед началом работы

Вот шаги, необходимые для хорошего опыта отладки памяти, повышения шансов найти дефекты и сокращения времени отладки:

  • Всегда используйте debug-сборку PHP. Пытаться отлаживать память на production-сборке бессмысленно.

  • Всегда запускайте отладчик с переменной окружения USE_ZEND_ALLOC=0. Как вы могли узнать в главе Zend Memory Manager, эта переменная окружения отключает ZendMM для текущего запуска процесса. Настоятельно рекомендуется делать это при запуске отладчика памяти. Полный обход ZendMM очень помогает в понимании трассировок, генерируемых valgrind.

  • Также настоятельно рекомендуется запускать отладчик памяти с переменной окружения ZEND_DONT_UNLOAD_MODULES=1. Это предотвратит выгрузку PHP файлов .so расширений в конце процесса. Это нужно для получения более качественных трассировок в отчёте valgrind: если бы PHP выгрузил расширения в тот момент, когда valgrind собирался вывести свои ошибки, эти ошибки были бы неполными, так как файл, из которого нужно брать информацию, больше не являлся бы частью образа памяти процесса.

  • Вам может понадобиться несколько suppression-файлов. Поскольку вы указываете PHP не выгружать свои расширения в конце процесса, в выводе valgrind могут появиться ложные срабатывания. Расширения PHP проверяются на утечки, и если на вашей платформе возникают ложные срабатывания, вы можете их подавить с помощью suppression-файла наподобие этого. Не стесняйтесь написать свой собственный файл по такому примеру.

  • Valgrind однозначно лучший инструмент, чем Zend Memory Manager, для поиска утечек и других проблем, связанных с памятью. Вы должны всегда прогонять valgrind на своём коде — это действительно обязательный шаг для каждого C-программиста. Вы запускаете его как потому, что получили краш и хотите найти и отладить его, так и в качестве инструмента контроля качества, когда на первый взгляд всё выглядит нормально — valgrind способен указать на скрытые дефекты, готовые взорваться вам в лицо сейчас или позже. Используйте его, даже если вам кажется, что с вашим кодом всё в порядке: вас может удивить результат.

Warning

Вы обязаны использовать valgrind (или любой другой отладчик памяти) для своей программы. Невозможно быть на 100% уверенным в любой надёжной программе на C, не отлаживая память. Ошибки памяти приводят к опасным проблемам безопасности и крашам программы, часто случайным образом, в зависимости от множества параметров.

Отладка ZendMM во время выполнения

Начиная с PHP 8.5, в ZendMM также появились вспомогательные средства отладки кучи, включаемые во время выполнения. Они полезны, когда повреждение памяти воспроизводится только в окружении, где debug-сборка, ASAN/MSAN или valgrind работают слишком медленно или иным образом непрактичны. Они не заменяют эти инструменты. Они заставляют повреждение кучи ZendMM приводить к краху раньше и с более полезным backtrace.

Включите их с помощью переменной окружения ZEND_MM_DEBUG. Она принимает список опций key=value, разделённых запятыми:

ZEND_MM_DEBUG=poison_free=0xbe,poison_alloc=0xeb,padding=16,check_freelists_on_shutdown=1 php ...

Поддерживаемые опции:

  • poison_free=byte: перезаписать освобождённые блоки ZendMM заданным значением байта.

  • poison_alloc=byte: перезаписать новые выделенные блоки ZendMM заданным значением байта.

  • padding=bytes: добавить неиспользуемые байты до и после каждой аллокации ZendMM. Значение должно быть выровнено по ZEND_MM_ALIGNMENT. 16 — хорошая отправная точка для типичных 64-битных сборок.

  • check_freelists_on_shutdown=0|1: обойти списки свободных блоков ZendMM во время завершения работы, чтобы обнаружить их повреждение.

Когда ZEND_MM_DEBUG включён, его обработчик realloc всегда выделяет новый блок и освобождает старый. Это может выявить код, который продолжает использовать старый указатель после перевыделения. Если ZEND_MM_DEBUG не задан, накладных расходов нет. Если установлен USE_ZEND_ALLOC=0, ZendMM отключается ещё до проверки ZEND_MM_DEBUG, поэтому эти специфичные для ZendMM средства отладки не устанавливаются.

Пример обнаружения утечки памяти

Начальный пример

Valgrind — это полноценный отладчик памяти кучи. Он также может отлаживать карты памяти процессов и стеки функций. Пожалуйста, обратитесь за дополнительной информацией к его документации.

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

PHP_RINIT_FUNCTION(pib)
{
    void *foo = emalloc(128);
}

Код выше утекает 128 байт при каждом запросе, потому что для этого буфера нет соответствующего вызова efree(). Поскольку это вызов emalloc() и, следовательно, он проходит через Zend Memory Manager, этот последний предупредит нас об утечке, как мы видели в главе о ZendMM. Давайте также посмотрим, может ли valgrind заметить эту утечку:

> ZEND_DONT_UNLOAD_MODULES=1 USE_ZEND_ALLOC=0 valgrind --leak-check=full --suppressions=/path/to/suppression
--show-reachable=yes --track-origins=yes ~/myphp/bin/php -dextension=pib.so /tmp/foo.php

Мы запускаем процесс PHP-CLI через valgrind. Здесь мы предполагаем расширение с именем «pib». Вот вывод:

==28104== 128 bytes in 1 blocks are definitely lost in loss record 1 of 1
==28104==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==28104==    by 0xA3701E: __zend_malloc (zend_alloc.c:2820)
==28104==    by 0xA362E7: _emalloc (zend_alloc.c:2413)
==28104==    by 0xE896F99: zm_activate_pib (pib.c:1880)
==28104==    by 0xA79F1B: zend_activate_modules (zend_API.c:2537)
==28104==    by 0x9D31D3: php_request_startup (main.c:1673)
==28104==    by 0xB5909A: do_cli (php_cli.c:964)
==28104==    by 0xB5A423: main (php_cli.c:1381)

==28104== LEAK SUMMARY:
==28104==    definitely lost: 128 bytes in 1 blocks
==28104==    indirectly lost: 0 bytes in 0 blocks
==28104==    possibly lost: 0 bytes in 0 blocks
==28104==    still reachable: 0 bytes in 0 blocks
==28104==    suppressed: 7,883 bytes in 40 blocks

На нашем уровне внимание нужно обращать именно на «definitely lost».

Note

За подробностями о различных полях, выводимых memcheck, пожалуйста, обратитесь к его документации.

Note

Мы использовали USE_ZEND_ALLOC=0, чтобы отключить и полностью обойти Zend Memory Manager. Каждый вызов его API (например, emalloc()) напрямую приводит к вызову libc, как видно по стековым фреймам в выводе valgrind.

Valgrind поймал нашу утечку.

Теперь, что довольно просто, мы можем сгенерировать утечку с помощью постоянной аллокации, то есть аллокации динамической памяти, минуя ZendMM и используя традиционный libc. Вперёд:

PHP_RINIT_FUNCTION(pib)
{
    void *foo = malloc(128);
}

Вот отчёт:

==28758==    128 bytes in 1 blocks are definitely lost in loss record 1 of 1
==28758==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==28758==    by 0xE896F82: zm_activate_pib (pib.c:1880)
==28758==    by 0xA79F1B: zend_activate_modules (zend_API.c:2537)
==28758==    by 0x9D31D3: php_request_startup (main.c:1673)
==28758==    by 0xB5909A: do_cli (php_cli.c:964)
==28758==    by 0xB5A423: main (php_cli.c:1381)

Поймано и здесь.

Note

Valgrind действительно ловит всё. Каждый маленький забытый байт где-то в ОГРОМНОЙ карте памяти процесса будет замечен бдительным взором valgrind. Мимо не пройдёшь.

Более сложный случай использования

Вот более сложный пример. Можете ли вы найти утечки в коде ниже?

static zend_array ar;

PHP_MINIT_FUNCTION(pib)
{
    zend_string *str;
    zval string;

    str = zend_string_init("yo", strlen("yo"), 1);
    ZVAL_STR(&string, str);

    zend_hash_init(&ar, 8, NULL, ZVAL_PTR_DTOR, 1);
    zend_hash_next_index_insert(&ar, &string);
}

Здесь две утечки. Во-первых, мы выделяем zend_string, но не освобождаем её. Во-вторых, мы выделяем новый zend_hash, но его мы тоже не освобождаем. Запустим это с valgrind и посмотрим на результат:

==31316== 296 (264 direct, 32 indirect) bytes in 1 blocks are definitely lost in loss record 1 of 2
==32006==    by 0xA3701E: __zend_malloc (zend_alloc.c:2820)
==32006==    by 0xA814B2: zend_hash_real_init_ex (zend_hash.c:133)
==32006==    by 0xA816D2: zend_hash_check_init (zend_hash.c:161)
==32006==    by 0xA83552: _zend_hash_index_add_or_update_i (zend_hash.c:714)
==32006==    by 0xA83D58: _zend_hash_next_index_insert (zend_hash.c:841)
==32006==    by 0xE896AF4: zm_startup_pib (pib.c:1781)
==32006==    by 0xA774F7: zend_startup_module_ex (zend_API.c:1843)
==32006==    by 0xA77559: zend_startup_module_zval (zend_API.c:1858)
==32006==    by 0xA85AF5: zend_hash_apply (zend_hash.c:1508)
==32006==    by 0xA77B25: zend_startup_modules (zend_API.c:1969)

==31316== 32 bytes in 1 blocks are indirectly lost in loss record 2 of 2
==31316==    by 0xA3701E: __zend_malloc (zend_alloc.c:2820)
==31316==    by 0xE880B0D: zend_string_alloc (zend_string.h:122)
==31316==    by 0xE880B76: zend_string_init (zend_string.h:158)
==31316==    by 0xE896F9D: zm_activate_pib (pib.c:1781)
==31316==    by 0xA79F1B: zend_activate_modules (zend_API.c:2537)
==31316==    by 0x9D31D3: php_request_startup (main.c:1673)
==31316==    by 0xB5909A: do_cli (php_cli.c:964)
==31316==    by 0xB5A423: main (php_cli.c:1381)

==31316== LEAK SUMMARY:
==31316== definitely lost: 328 bytes in 2 blocks

Как и ожидалось, обе утечки обнаружены. Как видите, valgrind точен — он направляет ваш взгляд именно туда, куда нужно.

Теперь исправим их:

PHP_MSHUTDOWN_FUNCTION(pib)
{
    zend_hash_destroy(&ar);
}

Мы уничтожаем постоянный массив в конце процесса PHP, в MSHUTDOWN. Поскольку при создании мы передали ему ZVAL_PTR_DTOR в качестве деструктора, он выполнит этот callback для каждого вставленного нами элемента. Это деструктор zval, который уничтожает zval-ы, анализируя их содержимое. Для типов IS_STRING деструктор освободит zend_string и, если нужно, освободит память. Готово.

Note

Как видите, PHP — как и любая серьёзная программа на C — полон вложенных указателей. zend_string заключён внутри zval, который сам является частью zend_array. Утечка массива, очевидно, приведёт к утечке и zval, и zend_string, но zvals не выделяются в куче (мы выделили их на стеке), поэтому об утечке самого zval отчёта не будет. Вам стоит привыкнуть к тому, что забытое освобождение составной структуры, такой как zend_array, приводит к целому ряду утечек, так как структуры часто вкладываются в структуры, которые вкладываются в другие структуры, и так далее…

Обнаружение переполнения/недополнения буфера

Утечка памяти — это плохо. Она приведёт к тому, что ваша программа рано или поздно вызовет OOM, и будет сильно замедлять хост-машину, поскольку той со временем становится доступно всё меньше и меньше памяти. Это синдром утечек памяти.

Но есть кое-что похуже: выход за границы буфера. Обращение к указателю за пределами границ аллокации лежит в основе стольких зловредных операций (таких как получение root-shell на машине), что вы обязательно должны их предотвращать. В более лёгком случае выход за границы также часто приводит к краху программы из-за повреждения памяти. Однако всё это зависит от целевой аппаратной машины, используемого компилятора и его опций, разметки памяти ОС, используемой libc и т.д. — множества факторов.

Таким образом, выходы за границы очень опасны — это бомбы, которые могут взорваться, а могут и не взорваться, прямо сейчас, через минуту, а если вам чрезвычайно повезёт — никогда.

Valgrind — это отладчик памяти, и поэтому он способен обнаруживать любой выход за границы в любой области памяти (куча и стек). Для этого используется тот же инструмент memcheck, что и для поиска утечек.

Рассмотрим простой пример:

PHP_MINIT_FUNCTION(pib)
{
    char *foo = malloc(16);
    foo[16] = 'a';
    foo[-1] = 'a';
}

Этот код выделяет буфер и намеренно записывает один байт за границей и один байт после границ буфера. Теперь если вы запустите такой код, у вас есть что-то около одного шанса из двух, что он рухнет немедленно, а в остальных случаях — случайным образом. Вы также могли создать дыру безопасности в PHP, но она может быть недоступна для удалённой эксплуатации (такое поведение остаётся нетипичным).

Warning

Выход за границы приводит к неопределённому поведению. Невозможно предсказать, что произойдёт, но будьте уверены — это плохо (немедленный крах) либо ужасно (проблема безопасности). Помните об этом.

Спросим valgrind, используя точно такую же командную строку для запуска, как и раньше — ничего не меняется, кроме вывода:

==12802== Invalid write of size 1
==12802==    at 0xE896A98: zm_startup_pib (pib.c:1772)
==12802==    by 0xA774F7: zend_startup_module_ex (zend_API.c:1843)
==12802==    by 0xA77559: zend_startup_module_zval (zend_API.c:1858)
==12802==    by 0xA85AF5: zend_hash_apply (zend_hash.c:1508)
==12802==    by 0xA77B25: zend_startup_modules (zend_API.c:1969)
==12802==    by 0x9D4541: php_module_startup (main.c:2260)
==12802==    by 0xB5802F: php_cli_startup (php_cli.c:427)
==12802==    by 0xB5A367: main (php_cli.c:1348)
==12802==  Address 0xeb488f0 is 0 bytes after a block of size 16 alloc'd
==12802==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12802==    by 0xE896A85: zm_startup_pib (pib.c:1771)
==12802==    by 0xA774F7: zend_startup_module_ex (zend_API.c:1843)
==12802==    by 0xA77559: zend_startup_module_zval (zend_API.c:1858)
==12802==    by 0xA85AF5: zend_hash_apply (zend_hash.c:1508)
==12802==    by 0xA77B25: zend_startup_modules (zend_API.c:1969)
==12802==    by 0x9D4541: php_module_startup (main.c:2260)
==12802==    by 0xB5802F: php_cli_startup (php_cli.c:427)
==12802==    by 0xB5A367: main (php_cli.c:1348)
==12802==
==12802== Invalid write of size 1
==12802==    at 0xE896AA6: zm_startup_pib (pib.c:1773)
==12802==    by 0xA774F7: zend_startup_module_ex (zend_API.c:1843)
==12802==    by 0xA77559: zend_startup_module_zval (zend_API.c:1858)
==12802==    by 0xA85AF5: zend_hash_apply (zend_hash.c:1508)
==12802==    by 0xA77B25: zend_startup_modules (zend_API.c:1969)
==12802==    by 0x9D4541: php_module_startup (main.c:2260)
==12802==    by 0xB5802F: php_cli_startup (php_cli.c:427)
==12802==    by 0xB5A367: main (php_cli.c:1348)
==12802==  Address 0xeb488df is 1 bytes before a block of size 16 alloc'd
==12802==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12802==    by 0xE896A85: zm_startup_pib (pib.c:1771)
==12802==    by 0xA774F7: zend_startup_module_ex (zend_API.c:1843)
==12802==    by 0xA77559: zend_startup_module_zval (zend_API.c:1858)
==12802==    by 0xA85AF5: zend_hash_apply (zend_hash.c:1508)
==12802==    by 0xA77B25: zend_startup_modules (zend_API.c:1969)
==12802==    by 0x9D4541: php_module_startup (main.c:2260)
==12802==    by 0xB5802F: php_cli_startup (php_cli.c:427)
==12802==    by 0xB5A367: main (php_cli.c:1348)

Обе некорректные записи были обнаружены, и теперь ваша задача — найти их и исправить.

Здесь мы использовали пример, где память записывается за её границами — это худший сценарий, поскольку ваша операция записи, если она успешна (хотя она может сразу привести к SIGSEGV), перезапишет некоторые критические области рядом с этим указателем. Поскольку мы выделяли память через libc-функцию malloc(), мы перезапишем критические головные и хвостовые блоки, которые libc использует для управления своими аллокациями и их отслеживания. В зависимости от многих факторов (платформа, используемая libc, способ компиляции и т.д.) это приведёт к краху.

Valgrind также может сообщать о некорректных чтениях. Это означает, что вы выполняете операцию чтения памяти за границами выделенного указателя. Это более благоприятный сценарий, чем перезапись блока, но вы всё равно обращаетесь к области памяти, к которой не должны обращаться, и здесь снова такой сценарий может привести к немедленному краху, краху позже, а может и никогда? Не делайте так.

Note

Как только вы видите «Invalid» в выводе valgrind, для вас это действительно плохой знак. Будь то некорректное чтение или запись, у вас проблема в коде, и её следует считать проблемой высокого риска: исправляйте её прямо сейчас, по-настоящему.

Вот второй пример, связанный с конкатенацией строк:

char *foo = strdup("foo");
char *bar = strdup("bar");

char *foobar = malloc(strlen("foo") + strlen("bar"));

memcpy(foobar, foo, strlen(foo));
memcpy(foobar + strlen("foo"), bar, strlen(bar));

fprintf(stderr, "%s", foobar);

free(foo);
free(bar);
free(foobar);

Можете найти проблему?

Спросим valgrind:

==13935== Invalid read of size 1
==13935==    at 0x4C30F74: strlen (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==13935==    by 0x768203E: fputs (iofputs.c:33)
==13935==    by 0xE896B91: zm_startup_pib (pib.c:1779)
==13935==    by 0xA774F7: zend_startup_module_ex (zend_API.c:1843)
==13935==    by 0xA77559: zend_startup_module_zval (zend_API.c:1858)
==13935==    by 0xA85AF5: zend_hash_apply (zend_hash.c:1508)
==13935==    by 0xA77B25: zend_startup_modules (zend_API.c:1969)
==13935==    by 0x9D4541: php_module_startup (main.c:2260)
==13935==    by 0xB5802F: php_cli_startup (php_cli.c:427)
==13935==    by 0xB5A367: main (php_cli.c:1348)
==13935==  Address 0xeb48986 is 0 bytes after a block of size 6 alloc'd
==13935==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==13935==    by 0xE896B14: zm_startup_pib (pib.c:1774)
==13935==    by 0xA774F7: zend_startup_module_ex (zend_API.c:1843)
==13935==    by 0xA77559: zend_startup_module_zval (zend_API.c:1858)
==13935==    by 0xA85AF5: zend_hash_apply (zend_hash.c:1508)
==13935==    by 0xA77B25: zend_startup_modules (zend_API.c:1969)
==13935==    by 0x9D4541: php_module_startup (main.c:2260)
==13935==    by 0xB5802F: php_cli_startup (php_cli.c:427)
==13935==    by 0xB5A367: main (php_cli.c:1348)

Строка 1779 указывает на вызов fprintf(). Этот вызов обратился к fputs(), который сам вызвал strlen() (обе функции из libc), и здесь strlen() некорректно читает 1 байт.

Мы просто забыли \0 для завершения строки. Мы передаём fprintf() невалидную строку. Сначала она пытается вычислить длину этой строки, вызывая strlen(). strlen() затем сканирует буфер, пока не найдёт \0, и будет сканировать за границами буфера, так как мы забыли завершить его нулём. Здесь нам повезло: strlen() выходит за конец всего на один байт. Это могло быть гораздо больше и могло привести к краху, потому что мы на самом деле не знаем, где в памяти окажется следующий \0 — это случайно.

Решение:

size_t len   = strlen("foo") + strlen("bar") + 1;   /* note the +1 for \0 */
char *foobar = malloc(len);

/* ... ... same code ... ... */

foobar[len - 1] = '\0'; /* terminate the string properly */

Note

Описанная выше ошибка — одна из самых распространённых в C. Такие ошибки называются off-by-one (ошибка на единицу): вы забываете выделить всего один байт, но именно из-за этого создаёте в коде целую кучу проблем.

Наконец, вот последний пример, демонстрирующий сценарий use-after-free. Это также очень распространённая ошибка в программировании на C, которая настолько же опасна, как и некорректный доступ к памяти: она создаёт уязвимости безопасности, способные приводить к очень неприятному поведению. Разумеется, valgrind может обнаруживать use-after-free. Вот пример:

char *foo = strdup("foo");
free(foo);

memcpy(foo, "foo", sizeof("foo"));

И снова сценарий из мира PHP, хотя формально он не имеет отношения к PHP как таковому. Мы освобождаем указатель, а затем используем его снова. Это серьёзная ошибка. Спросим valgrind:

==14594== Invalid write of size 1
==14594==    at 0x4C3245C: memcpy@GLIBC_2.2.5 (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==14594==    by 0xE896AA1: zm_startup_pib (pib.c:1774)
==14594==    by 0xA774F7: zend_startup_module_ex (zend_API.c:1843)
==14594==    by 0xA77559: zend_startup_module_zval (zend_API.c:1858)
==14594==    by 0xA85AF5: zend_hash_apply (zend_hash.c:1508)
==14594==    by 0xA77B25: zend_startup_modules (zend_API.c:1969)
==14594==    by 0x9D4541: php_module_startup (main.c:2260)
==14594==    by 0xB5802F: php_cli_startup (php_cli.c:427)
==14594==    by 0xB5A367: main (php_cli.c:1348)
==14594==  Address 0xeb488e0 is 0 bytes inside a block of size 4 free'd
==14594==    at 0x4C2EDEB: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==14594==    by 0xE896A86: zm_startup_pib (pib.c:1772)
==14594==    by 0xA774F7: zend_startup_module_ex (zend_API.c:1843)
==14594==    by 0xA77559: zend_startup_module_zval (zend_API.c:1858)
==14594==    by 0xA85AF5: zend_hash_apply (zend_hash.c:1508)
==14594==    by 0xA77B25: zend_startup_modules (zend_API.c:1969)
==14594==    by 0x9D4541: php_module_startup (main.c:2260)
==14594==    by 0xB5802F: php_cli_startup (php_cli.c:427)
==14594==    by 0xB5A367: main (php_cli.c:1348)
==14594==  Block was alloc'd at
==14594==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==14594==    by 0x769E8D9: strdup (strdup.c:42)
==14594==    by 0xE896A70: zm_startup_pib (pib.c:1771)
==14594==    by 0xA774F7: zend_startup_module_ex (zend_API.c:1843)
==14594==    by 0xA77559: zend_startup_module_zval (zend_API.c:1858)
==14594==    by 0xA85AF5: zend_hash_apply (zend_hash.c:1508)
==14594==    by 0xA77B25: zend_startup_modules (zend_API.c:1969)
==14594==    by 0x9D4541: php_module_startup (main.c:2260)
==14594==    by 0xB5802F: php_cli_startup (php_cli.c:427)
==14594==    by 0xB5A367: main (php_cli.c:1348)

Здесь снова всё предельно ясно.

Выводы

Используйте отладчик памяти перед выпуском в production. Как вы узнали из этой главы, крошечный байт, забытый в ваших вычислениях, может привести к эксплуатируемой уязвимости безопасности. Также он часто (очень часто) приводит просто к краху. Это означает, что ваше замечательное расширение может обрушить целый сервер (или набор серверов) и всех его клиентов.

C — очень строгий язык программирования. Вам даются миллиарды байт памяти для программирования, и вы должны упорядочить их для выполнения тех или иных вычислений. Но не злоупотребляйте этой огромной силой: в лучшем случае (редко) ничего не произойдёт, в худшем случае (очень часто) вы будете случайным образом получать краши там и сям, а в наихудшем сценарии вы создадите в программе брешь, которая окажется удалённо эксплуатируемой…

У вас есть инструменты и ум — пожалуйста, по-настоящему заботьтесь о памяти машины.