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.

Found yet Another Memory Leak in PHP
Tagged on:         

Leave a Reply

Your email address will not be published. Required fields are marked *