10. Catching Memory Leaks
Let’s try to pass array with a value of some unexpected type:
$ php -r ‘var_dump(test_scale([null]));’
Warning: test_scale(): unexpected argument type in Command line code on line 1
NULL
[Wed Jan 22 13:56:11 2020] Script: ‘Standard input code’
/home/dmitry/tmp/php-src/Zend/zend_hash.c(256) : Freeing 0x00007f8189c57840 (56 bytes), script=Standard
input code
=== Total 1 memory leaks detected ===
We see our expected warning and NULL result, but then we see some debug info about leaked memory from internal PHP memory debugger. Note, that this information is only available in DEBUG PHP build, and this is one of the reasons, I recommend, to use DEBUG build during development. The information above says that 56-bytes of memory allocated on line 256 of Zend/zend_hash.c was leaked. This is the body of _zend_new_array() and we may already guess where it’s called from, because we call it just once. However, in real-life we can’t be sure about the call site, and it would be great to get a back-trace of the leaked allocation.
On Linux we may use valgrind. It’s a great tool, that can catch memory-leaks and other incorrect memory access problems (e.g. use-after-free and out-of-boundary). Valgrind emulates the program with an overridden system memory manager (malloc, free and related functions) and catches inconsistencies.
Looking ahead, PHP uses its own memory manager and we should switch to system one, using USE_ZEND_ALLOC environment variable. It also makes sense to disable extension unloading.
$ USE_ZEND_ALLOC=0 ZEND_DONT_UNLOAD_MODULES=1 valgrind --leak-check=full \
php -r ‘var_dump(test_scale([null]));’
...
==19882== 56 bytes in 1 blocks are definitely lost in loss record 19 of 27
==19882== at 0x483880B: malloc (vg_replace_malloc.c:309)
==19882== by 0x997CC5: __zend_malloc (zend_alloc.c:2975)
==19882== by 0x996C30: _malloc_custom (zend_alloc.c:2416)
==19882== by 0x996D6E: _emalloc (zend_alloc.c:2535)
==19882== by 0x9E13BE: _zend_new_array (zend_hash.c:256)
==19882== by 0x4849AE0: do_scale (test.c:66)
==19882== by 0x4849F69: zif_test_scale (test.c:100)
==19882== by 0xA3CE1B: ZEND_DO_ICALL_SPEC_RETVAL_USED_HANDLER (zend_vm_execute.h:1313)
==19882== by 0xA9D0E8: execute_ex (zend_vm_execute.h:53564)
==19882== by 0xAA11A0: zend_execute (zend_vm_execute.h:57664)
==19882== by 0x9B7D0B: zend_eval_stringl (zend_execute_API.c:1082)
==19882== by 0x9B7EBF: zend_eval_stringl_ex (zend_execute_API.c:1123)
...
Now we can be sure: the source of our memory-leak is a call of zend_new_array() function from our do_scale(). To fix it, we should destroy the array in case of FAILURE.
} else if (Z_TYPE_P(x) == IS_ARRAY) {
zend_array *ret = zend_new_ array(zend_array_count(Z_ARR_P(x)));
zend_ulong idx;
zend_string *key;
zval *val, tmp;
ZEND_HASH_FOREACH_KEY_VAL(Z_ARR_P(x), idx, key, val) {
if (do_scale(&tmp, val, factor) != SUCCESS) {
zend_array_destroy(ret);
return FAILURE;
}
if (key) {
zend_hash_add(ret, key, &tmp);
} else {
zend_hash_index_add(ret, idx, &tmp);
}
} ZEND_HASH_FOREACH_END();
RETVAL_ARR(ret);
} else {
Don’t forget to test this.
Valgrind is much smarter then the internal PHP memory debugger and in case you cover your extension with *.phpt regression tests, you may run all of them under valgrind.
$ make test TESTS=”-m”