PHP extension use of uninitialized memory on second request

Viewed 160

I have written a PHP extension, it works fine. However currently, I'm facing a strange issue. I have a function implemented for "require" files in PHP. There are no memory leaks for the function on the first request, but the memory leak happens on the second request. It makes me feel bad since I don't know whether I went with right way. I thought that it could be caused because I didn't handle right thing for "shutdown" the extension. However, on other hand, I think the PHP will handle each request separately, how can the first request cause the problem for the second request, it makes me confused.

Here is my function:

zval requireFile(char *path) {
    zend_file_handle fileHandle;
    zend_op_array *opArray;
    zval dummy, output;

    ZVAL_UNDEF(&output);

    php_stream_open_for_zend_ex(path, &fileHandle, USE_PATH|STREAM_OPEN_FOR_INCLUDE);

    opArray = zend_compile_file(&fileHandle, ZEND_REQUIRE);

    if (opArray) {
        if (fileHandle.handle.stream.handle) {
            ZVAL_NULL(&dummy);
            if (!fileHandle.opened_path) {
                fileHandle.opened_path = zend_string_init(path, strlen(path), 0);
            }

            zend_hash_add(&EG(included_files), fileHandle.opened_path, &dummy);
            zend_destroy_file_handle(&fileHandle);
        }

        opArray->scope = EG(fake_scope) ? EG(fake_scope) : zend_get_executed_scope();
        zend_execute(opArray, &output);

        destroy_op_array(opArray);
        efree_size(opArray, sizeof(zend_op_array));
    } else {
        zend_destroy_file_handle(&fileHandle);
    }

    return output;
}

Here is the valgrind output if I request more than one request to the server:

==3202== Memcheck, a memory error detector
==3202== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==3202== Using Valgrind-3.17.0 and LibVEX; rerun with -h for copyright info
==3202== Command: /root/php-bin/DEBUG/bin/php -S localhost:8000 test/index.php
==3202== Parent PID: 10
==3202== 
==3202== Conditional jump or move depends on uninitialised value(s)
==3202==    at 0x71B6220: persistent_compile_file (ZendAccelerator.c:2217)
==3202==    by 0x72A4A77: requireFile (require.c:13)
==3202==    by 0x72A8293: zim_I18n_init (i18n.c:39)
==3202==    by 0x76651B: ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER (zend_vm_execute.h:1755)
==3202==    by 0x7E8E9F: execute_ex (zend_vm_execute.h:55172)
==3202==    by 0x7ED5A7: zend_execute (zend_vm_execute.h:59499)
==3202==    by 0x725AAB: zend_execute_scripts (zend.c:1694)
==3202==    by 0x83DF3F: php_cli_server_dispatch_router (php_cli_server.c:2168)
==3202==    by 0x83E153: php_cli_server_dispatch (php_cli_server.c:2208)
==3202==    by 0x83ED67: php_cli_server_recv_event_read_request (php_cli_server.c:2529)
==3202==    by 0x83F15B: php_cli_server_do_event_for_each_fd_callback (php_cli_server.c:2615)
==3202==    by 0x83A913: php_cli_server_poller_iter_on_active (php_cli_server.c:869)
==3202==  Uninitialised value was created by a heap allocation
==3202==    at 0x484EFC8: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-arm64-linux.so)
==3202==    by 0x6E79C7: __zend_malloc (zend_alloc.c:3056)
==3202==    by 0x6E649B: _malloc_custom (zend_alloc.c:2418)
==3202==    by 0x6E660F: _emalloc (zend_alloc.c:2537)
==3202==    by 0x753D07: zend_vm_stack_new_page (zend_execute.c:184)
==3202==    by 0x753D6B: zend_vm_stack_init (zend_execute.c:195)
==3202==    by 0x709BE7: init_executor (zend_execute_API.c:148)
==3202==    by 0x7240EF: zend_activate (zend.c:1212)
==3202==    by 0x67510B: php_request_startup (main.c:1725)
==3202==    by 0x83DDEB: php_cli_server_request_startup (php_cli_server.c:2130)
==3202==    by 0x83E0FF: php_cli_server_dispatch (php_cli_server.c:2199)
==3202==    by 0x83ED67: php_cli_server_recv_event_read_request (php_cli_server.c:2529)
==3202== 
==3202== Conditional jump or move depends on uninitialised value(s)
==3202==    at 0x71B6220: persistent_compile_file (ZendAccelerator.c:2217)
==3202==    by 0x72A4A77: requireFile (require.c:13)
==3202==    by 0x72A841B: zim_I18n_init (i18n.c:55)
==3202==    by 0x76651B: ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER (zend_vm_execute.h:1755)
==3202==    by 0x7E8E9F: execute_ex (zend_vm_execute.h:55172)
==3202==    by 0x7ED5A7: zend_execute (zend_vm_execute.h:59499)
==3202==    by 0x725AAB: zend_execute_scripts (zend.c:1694)
==3202==    by 0x83DF3F: php_cli_server_dispatch_router (php_cli_server.c:2168)
==3202==    by 0x83E153: php_cli_server_dispatch (php_cli_server.c:2208)
==3202==    by 0x83ED67: php_cli_server_recv_event_read_request (php_cli_server.c:2529)
==3202==    by 0x83F15B: php_cli_server_do_event_for_each_fd_callback (php_cli_server.c:2615)
==3202==    by 0x83A913: php_cli_server_poller_iter_on_active (php_cli_server.c:869)
==3202==  Uninitialised value was created by a heap allocation
==3202==    at 0x484EFC8: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-arm64-linux.so)
==3202==    by 0x6E79C7: __zend_malloc (zend_alloc.c:3056)
==3202==    by 0x6E649B: _malloc_custom (zend_alloc.c:2418)
==3202==    by 0x6E660F: _emalloc (zend_alloc.c:2537)
==3202==    by 0x753D07: zend_vm_stack_new_page (zend_execute.c:184)
==3202==    by 0x753D6B: zend_vm_stack_init (zend_execute.c:195)
==3202==    by 0x709BE7: init_executor (zend_execute_API.c:148)
==3202==    by 0x7240EF: zend_activate (zend.c:1212)
==3202==    by 0x67510B: php_request_startup (main.c:1725)
==3202==    by 0x83DDEB: php_cli_server_request_startup (php_cli_server.c:2130)
==3202==    by 0x83E0FF: php_cli_server_dispatch (php_cli_server.c:2199)
==3202==    by 0x83ED67: php_cli_server_recv_event_read_request (php_cli_server.c:2529)
==3202== 
==3202== 
==3202== HEAP SUMMARY:
==3202==     in use at exit: 3,501 bytes in 10 blocks
==3202==   total heap usage: 15,425 allocs, 15,415 frees, 3,619,041 bytes allocated
==3202== 
==3202== LEAK SUMMARY:
==3202==    definitely lost: 0 bytes in 0 blocks
==3202==    indirectly lost: 0 bytes in 0 blocks
==3202==      possibly lost: 0 bytes in 0 blocks
==3202==    still reachable: 3,501 bytes in 10 blocks
==3202==         suppressed: 0 bytes in 0 blocks
==3202== Reachable blocks (those to which a pointer was found) are not shown.
==3202== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==3202== 
==3202== For lists of detected and suppressed errors, rerun with: -s
==3202== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
2 Answers

I know nothing about these libraries, but my guess is that if the following stream open fails (say the path does not exist)

zend_file_handle fileHandle;

...

php_stream_open_for_zend_ex(path, &fileHandle, USE_PATH|STREAM_OPEN_FOR_INCLUDE);

Then fileHandle could be used uninitialized in zend_compile_file

opArray = zend_compile_file(&fileHandle, ZEND_REQUIRE);

Assuming that php_stream_open_for_zend_ex returns some sort of boolean (true for success, false for failure) then try writing something like

if (php_stream_open_for_zend_ex(path, &fileHandle, USE_PATH|STREAM_OPEN_FOR_INCLUDE))
{
    // existing code
}
else
{
    // some new code to handle failure
}

EDIT:

I can't help much with the actual problem, which seems to be in the 'zend' code, I know nothing about PHP or Zend.

The best way to debug this is with vgdb server. Try the following:

  1. Open 2 terminals. I'm assuming that you are using some sort of windowing environment.
  2. In the first terminal, run Valgrind with valgrind --vgdb-error=0 your_app your_app_arguments It will print out some instructions and stop.
  3. In the second terminal, run gdb your_app and then copy and paste the command that Valgrind printed at the end of step 2.

Now in the gdb terminal you can use gdb more or less like normal debugging (except that you can't 'run', you need go through the steps above each time you start a valgrind.gdb session).

If you 'c[ontinue]', Valgrind will run until it encounters the first error.

Then you can use the usual gdb commands: 'p' to print variables, 'up' and 'down' to navigate the call stack.

You can also use 'monitor' commands to get info from Valgrind on the state of memory (whether or not is it accessible or initialized).

See here for a desciption of the memcheck monitor commands.

The issue happens inside the core in the file ZendAccelerator.c:2217

if (persistent_script->script.filename) {
    if (!EG(current_execute_data) || !EG(current_execute_data)->opline ||
        !EG(current_execute_data)->func ||
        !ZEND_USER_CODE(EG(current_execute_data)->func->common.type) ||
        EG(current_execute_data)->opline->opcode != ZEND_INCLUDE_OR_EVAL ||
        (EG(current_execute_data)->opline->extended_value != ZEND_INCLUDE_ONCE &&
            EG(current_execute_data)->opline->extended_value != ZEND_REQUIRE_ONCE)) {

For temporary fixing, we can set the fileHandle.filename to NULL; or disable the opcache.

The issue will not happen with the real environment because with the real environment, we will use the Zend for memory management, it will clean the memory after finish the request.

Update: The issue is fixed here https://github.com/php/php-src/commit/e488f7b0eb5aa0dbc396a17821386d914899e988

Related