When testing your own PHP extensions, it is very important not to miss any memory leaks. Wherever Valgrind shows a memory leak, you need to check your code if everything is deallocated correctly, that garbage collection handlers are present and return all necessary information, etc. However, sometimes you can find a memory leak in the PHP Core.
Here is a simple test case to reproduce the leak:
<?php class LeakTest extends \PHPUnit\Framework\TestCase { /** * @expectedException \RuntimeException */ public function testLeak() { throw new \RuntimeException(); } }
When using Valgrind or debug builds of PHP, it is very important to provide a clean shutdown: the script should terminate normally, without exit()
or die()
. I use my custom phpunit
script for that:
#!/usr/bin/env php <?php require __DIR__ . '/vendor/autoload.php'; $code = PHPUnit\TextUI\Command::main(false); if ($code) { exit($code); }
Passing false
to main()
prevents PHPUnit from calling exit()
.
To allow Valgrind to detect memory leaks properly, USE_ZEND_ALLOC
environment variable must be set to 0.
USE_ZEND_ALLOC=0 valgrind --leak-check=full $(phpenv which php) ./phpunit
Valgrind show something like this:
==13677== HEAP SUMMARY: ==13677== in use at exit: 1,774 bytes in 33 blocks ==13677== total heap usage: 81,556 allocs, 81,523 frees, 14,308,544 bytes allocated ==13677== ==13677== 72 bytes in 1 blocks are definitely lost in loss record 29 of 33 ==13677== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==13677== by 0x69DB58: __zend_malloc (zend_alloc.c:2838) ==13677== by 0x70C5F7: zend_objects_new (zend_objects.c:171) ==13677== by 0x6D1DE0: _object_and_properties_init (zend_API.c:1295) ==13677== by 0x77ADD5: ZEND_NEW_SPEC_UNUSED_HANDLER (zend_vm_execute.h:27938) ==13677== by 0x71D4A2: execute_ex (zend_vm_execute.h:429) ==13677== by 0x781334: zend_execute (zend_vm_execute.h:474) ==13677== by 0x6CEED1: zend_execute_scripts (zend.c:1482) ==13677== by 0x65C8EF: php_execute_script (main.c:2577) ==13677== by 0x783935: do_cli (php_cli.c:993) ==13677== by 0x250652: main (php_cli.c:1381) ==13677== ==13677== LEAK SUMMARY: ==13677== definitely lost: 72 bytes in 1 blocks ==13677== indirectly lost: 0 bytes in 0 blocks ==13677== possibly lost: 0 bytes in 0 blocks ==13677== still reachable: 1,702 bytes in 32 blocks ==13677== suppressed: 0 bytes in 0 blocks ==13677== Reachable blocks (those to which a pointer was found) are not shown. ==13677== To see them, rerun with: --leak-check=full --show-leak-kinds=all ==13677== ==13677== For counts of detected and suppressed errors, rerun with: -v ==13677== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
If we replace throw new \RuntimeException();
with something like $this->assertTrue(true);
and remove @expectedException
annotation, the leak will go away.
The interesting thing with this bug is that it manifests itself only for release PHP builds (the ones built with --disbale-debug
); debug builds (--enable-debug
) are not affected by this bug.
So far, I have confirmed that PHP 7.0.30 (and possible earlier versions) and PHP 7.1.11 — 7.1.17 (and possibly earlier versions) are affected. PHP 7.2.x is not affected (checked 7.2.0 and 7.2.5).
I have filed a bug report, hopefully this issue will be solved soon.
GitHub repository with the code to reproduce the bug.