Skip to content

PHP Fatal error triggers pointer being freed was not allocated and malloc: double free for ptr errors #11078

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
lucasnetau opened this issue Apr 15, 2023 · 44 comments

Comments

@lucasnetau
Copy link
Contributor

Description

The following code:

https://gist.github.com/lucasnetau/244ba31f321307e06177f48582273e86

Resulted in this output:

php(95590,0x7ff847fe5680) malloc: double free for ptr 0x7fd2c0198000

php(95560,0x7ff847fe5680) malloc: *** error for object 0x7fc2e801800a: pointer being freed was not allocated

SIGABRT

To trigger the issue a PHP Fatal error (out of memory) needs to occur in a write to a custom streamWrapper, the test case forces this quickly (pointer issue occurs on each run, double free occurs on most runs but not all). Notably when CURLOPT_HTTPHEADER is set the double free occurs in libcurl, when CURLOPT_HTTPHEADER is not set then the point being freed error occurs.

Double Free backtrace:

* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGABRT
    frame #0: 0x00007ff80467022a libsystem_kernel.dylib`__pthread_kill + 10
libsystem_kernel.dylib`:
->  0x7ff80467022a <+10>: jae    0x7ff804670234            ; <+20>
    0x7ff80467022c <+12>: movq   %rax, %rdi
    0x7ff80467022f <+15>: jmp    0x7ff804669ce2            ; cerror_nocancel
    0x7ff804670234 <+20>: retq   
Target 0: (php) stopped.
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGABRT
  * frame #0: 0x00007ff80467022a libsystem_kernel.dylib`__pthread_kill + 10
    frame #1: 0x00007ff8046a7f7b libsystem_pthread.dylib`pthread_kill + 263
    frame #2: 0x00007ff8045f1ca5 libsystem_c.dylib`abort + 123
    frame #3: 0x00007ff804507637 libsystem_malloc.dylib`malloc_vreport + 888
    frame #4: 0x00007ff80451c897 libsystem_malloc.dylib`malloc_zone_error + 178
    frame #5: 0x0000000101b920ce libcurl.4.dylib`Curl_close(datap=0x00007ff7bfefe9d0) at url.c:410:3 [opt]
    frame #6: 0x0000000101b565e0 libcurl.4.dylib`curl_easy_cleanup(data=0x0000000000000000) at easy.c:804:3 [opt]
    frame #7: 0x00000001000735dd php`curl_free_obj(object=0x0000000103bcd798) at interface.c:2839:2 [opt]
    frame #8: 0x000000010047b31e php`zend_objects_store_del(object=0x0000000103bcd798) at zend_objects_API.c:200:4 [opt]
    frame #9: 0x00000001003d26af php`zend_llist_destroy(l=0x0000000103bc70a8) at zend_llist.c:109:4 [opt]
    frame #10: 0x00000001003d2703 php`zend_llist_clean(l=0x0000000103bc70a8) at zend_llist.c:123:2 [opt]
    frame #11: 0x000000010007ab71 php`curl_multi_free_obj(object=<unavailable>) at multi.c:555:2 [opt]
    frame #12: 0x000000010047b167 php`zend_objects_store_free_object_storage(objects=<unavailable>, fast_shutdown=<unavailable>) at zend_objects_API.c:122:6 [opt]
    frame #13: 0x00000001003cf2f2 php`zend_shutdown_executor_values(fast_shutdown=<unavailable>) at zend_execute_API.c:399:2 [opt]
    frame #14: 0x00000001003cf3d8 php`shutdown_executor at zend_execute_API.c:416:2 [opt]
    frame #15: 0x00000001003df2bc php`zend_deactivate at zend.c:1258:2 [opt]
    frame #16: 0x000000010037a1e5 php`php_request_shutdown(dummy=0x0000000000000000) at main.c:1863:2 [opt]
    frame #17: 0x00000001004c41dc php`do_cli(argc=4, argv=0x0000600000c08c90) at php_cli.c:1135:3 [opt]
    frame #18: 0x00000001004c1e81 php`main(argc=4, argv=0x0000600000c08c90) at php_cli.c:1333:18 [opt]
    frame #19: 0x00007ff804375310 dyld`start + 2432
(lldb) f 5
warning: libcurl.4.dylib was compiled with optimization - stepping may behave oddly; variables may not be available.
frame #5: 0x0000000101b920ce libcurl.4.dylib`Curl_close(datap=0x00007ff7bfefe9d0) at url.c:410:3 [opt]
   407    up_free(data);
   408    Curl_safefree(data->state.buffer);
   409    Curl_dyn_free(&data->state.headerb);
-> 410    Curl_safefree(data->state.ulbuf);
   411    Curl_flush_cookies(data, TRUE);
   412    Curl_altsvc_save(data, data->asi, data->set.str[STRING_ALTSVC]);
   413    Curl_altsvc_cleanup(&data->asi);
(lldb) f 6
frame #6: 0x0000000101b565e0 libcurl.4.dylib`curl_easy_cleanup(data=0x0000000000000000) at easy.c:804:3 [opt]
   801      return;
   802  
   803    sigpipe_ignore(data, &pipe_st);
-> 804    Curl_close(&data);
   805    sigpipe_restore(&pipe_st);
   806  }
   807  
(lldb) f 7
warning: php was compiled with optimization - stepping may behave oddly; variables may not be available.
frame #7: 0x00000001000735dd php`curl_free_obj(object=0x0000000103bcd798) at interface.c:2839:2 [opt]
   2836         curl_easy_setopt(ch->cp, CURLOPT_HEADERFUNCTION, curl_write_nothing);
   2837         curl_easy_setopt(ch->cp, CURLOPT_WRITEFUNCTION, curl_write_nothing);
   2838 
-> 2839         curl_easy_cleanup(ch->cp);
   2840 
   2841         /* cURL destructors should be invoked only by last curl handle */
   2842         if (--(*ch->clone) == 0) {

Pointer being freed was not allocated:

* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGABRT
    frame #0: 0x00007ff80467022a libsystem_kernel.dylib`__pthread_kill + 10
libsystem_kernel.dylib`:
->  0x7ff80467022a <+10>: jae    0x7ff804670234            ; <+20>
    0x7ff80467022c <+12>: movq   %rax, %rdi
    0x7ff80467022f <+15>: jmp    0x7ff804669ce2            ; cerror_nocancel
    0x7ff804670234 <+20>: retq   
Target 0: (php) stopped.
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGABRT
  * frame #0: 0x00007ff80467022a libsystem_kernel.dylib`__pthread_kill + 10
    frame #1: 0x00007ff8046a7f7b libsystem_pthread.dylib`pthread_kill + 263
    frame #2: 0x00007ff8045f1ca5 libsystem_c.dylib`abort + 123
    frame #3: 0x00007ff804507637 libsystem_malloc.dylib`malloc_vreport + 888
    frame #4: 0x00007ff80450a951 libsystem_malloc.dylib`malloc_report + 151
    frame #5: 0x00007ff804595860 libsystem_c.dylib`fclose + 140
    frame #6: 0x0000000100391f0b php`stream_resource_regular_dtor(rsrc=<unavailable>) at streams.c:1761:19 [opt]
    frame #7: 0x00000001003f539a php`zend_resource_dtor(res=<unavailable>) at zend_list.c:73:3 [opt]
    frame #8: 0x00000001003f56df php`zend_close_rsrc_list(ht=0x0000000100ed5c30) at zend_list.c:225:5 [opt]
    frame #9: 0x00000001003cf5dc php`zend_shutdown_executor_values(fast_shutdown=<unavailable>) at zend_execute_API.c:277:3 [opt]
    frame #10: 0x00000001003cfcd8 php`shutdown_executor at zend_execute_API.c:416:2 [opt]
    frame #11: 0x00000001003dfbbc php`zend_deactivate at zend.c:1258:2 [opt]
    frame #12: 0x000000010037aae5 php`php_request_shutdown(dummy=0x0000000000000000) at main.c:1863:2 [opt]
    frame #13: 0x00000001004c4adc php`do_cli(argc=2, argv=0x00006000002080e0) at php_cli.c:1135:3 [opt]
    frame #14: 0x00000001004c2781 php`main(argc=2, argv=0x00006000002080e0) at php_cli.c:1333:18 [opt]
    frame #15: 0x00007ff804375310 dyld`start + 2432
(lldb) f 6
warning: php was compiled with optimization - stepping may behave oddly; variables may not be available.
frame #6: 0x0000000100391f0b php`stream_resource_regular_dtor(rsrc=<unavailable>) at streams.c:1761:19 [opt]
   1758 {
   1759         php_stream *stream = (php_stream*)rsrc->ptr;
   1760         /* set the return value for pclose */
-> 1761         FG(pclose_ret) = php_stream_free(stream, PHP_STREAM_FREE_CLOSE | PHP_STREAM_FREE_RSRC_DTOR);
   1762 }
   1763 
   1764 static void stream_resource_persistent_dtor(zend_resource *rsrc)
(lldb) f 7 
frame #7: 0x00000001003f539a php`zend_resource_dtor(res=<unavailable>) at zend_list.c:73:3 [opt]
   70           ZEND_ASSERT(ld && "Unknown list entry type");
   71   
   72           if (ld->list_dtor_ex) {
-> 73                   ld->list_dtor_ex(&r);
   74           }
   75   }
   76 

Double free was reported to curl via curl/curl#10964, however since a subtle change in settings (CURLOPT_HTTPHEADER) triggers a crash in PHP I am posting both here.

With ext-curl compiled with PHP_CURL_DEBUG=0 I only see the DTOR being called once (DTOR CALLED, ch = *)

The comments in interface.c before where the crash occurs for Curl are interesting
https://github.com/php/php-src/blob/master/ext/curl/interface.c#L2829-2838

PHP Version

PHP 8.2.5

Operating System

MacOS / Linux

@cgzones
Copy link

cgzones commented Apr 17, 2023

Might be a duplicate of #10670

@iluuu1994
Copy link
Member

Might be a duplicate of #10670

This isn't trying to use custom allocators.

I'm unable to trigger a crash, ASAN is also not showing anything. What CURL version are you using? I'm trying this on Linux.

@lucasnetau
Copy link
Contributor Author

Latest Curl 8.0.1 is used. So far I've triggered the crash on both MacOS and Linux (Docker). The crash is timing dependant so the rate of filling the buffer may need to change depending on your network, it does appear that Curl needs to be past a certain point in the transfer stage for the issue to occur.

I'm away from my main computer for a week, I'll come back the. with a code dump from Linux.

Is there any other flags I should use for building or running php to help?

@iluuu1994
Copy link
Member

That would be great, thank you!

Is there any other flags I should use for building or running php to help?

You could try the --enable-address-sanitizer and --enable-undefined-sanitizer flags.

@lucasnetau
Copy link
Contributor Author

Additional address sanitiser output on MacOS

Double free output

DTOR CALLED, ch = 1b06c000
=================================================================
==51504==ERROR: AddressSanitizer: attempting double-free on 0x631000014800 in thread T0:
    #0 0x1169e7019 in wrap_free+0xa9 (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x4b019)
    #1 0x115b6cee2 in Curl_close url.c:432
    #2 0x115b31ae8 in curl_easy_cleanup easy.c:804
    #3 0x10e8657ca in curl_free_obj interface.c:2840
    #4 0x111c2b3cb in zend_objects_store_del zend_objects_API.c:200
    #5 0x1111f28b1 in rc_dtor_func zend_variables.c:57
    #6 0x1111f2b3c in i_zval_ptr_dtor zend_variables.h:44
    #7 0x1111f28f4 in zval_ptr_dtor zend_variables.c:84
    #8 0x10e8b81fc in _php_curl_multi_cleanup_list multi.c:109
    #9 0x11115a2be in zend_llist_destroy zend_llist.c:109
    #10 0x11115a5d4 in zend_llist_clean zend_llist.c:123
    #11 0x10e8c8748 in curl_multi_free_obj multi.c:555
    #12 0x111c28d5b in zend_objects_store_free_object_storage zend_objects_API.c:122
    #13 0x111129469 in zend_shutdown_executor_values zend_execute_API.c:399
    #14 0x11112a9ab in shutdown_executor zend_execute_API.c:416
    #15 0x111203029 in zend_deactivate zend.c:1258
    #16 0x110d77981 in php_request_shutdown main.c:1863
    #17 0x11218b810 in do_cli php_cli.c:1135
    #18 0x11218573d in main php_cli.c:1333
    #19 0x7ff80437530f  (<unknown module>)

0x631000014800 is located 0 bytes inside of 65536-byte region [0x631000014800,0x631000024800)
freed by thread T0 here:
    #0 0x1169e7019 in wrap_free+0xa9 (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x4b019)
    #1 0x7ff80459585f in fclose+0x8b (libsystem_c.dylib:x86_64+0x2585f)
    #2 0x1169d8a4f in wrap_fclose+0x8f (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x3ca4f)
    #3 0x110e336d2 in _php_stream_free streams.c:475
    #4 0x110e4a609 in stream_resource_regular_dtor streams.c:1761
    #5 0x111318ddb in zend_resource_dtor zend_list.c:73
    #6 0x11131b09c in zend_close_rsrc_list zend_list.c:225
    #7 0x111122434 in zend_shutdown_executor_values zend_execute_API.c:277
    #8 0x11112a9ab in shutdown_executor zend_execute_API.c:416
    #9 0x111203029 in zend_deactivate zend.c:1258
    #10 0x110d77981 in php_request_shutdown main.c:1863
    #11 0x11218b810 in do_cli php_cli.c:1135
    #12 0x11218573d in main php_cli.c:1333
    #13 0x7ff80437530f  (<unknown module>)

previously allocated by thread T0 here:
    #0 0x1169e6ed0 in wrap_malloc+0xa0 (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x4aed0)
    #1 0x115b6b95a in Curl_readwrite transfer.c:1120
    #2 0x115b54b91 in multi_runsingle multi.c:2436
    #3 0x115b54186 in curl_multi_perform multi.c:2714
    #4 0x10e8bde53 in zif_curl_multi_exec multi.c:225
    #5 0x111852da5 in ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER zend_vm_execute.h:1250
    #6 0x1113c4462 in execute_ex zend_vm_execute.h:55823
    #7 0x1113c5820 in zend_execute zend_vm_execute.h:60396
    #8 0x11120e4c9 in zend_execute_scripts zend.c:1826
    #9 0x110d8218b in php_execute_script main.c:2542
    #10 0x11218944f in do_cli php_cli.c:964
    #11 0x11218573d in main php_cli.c:1333
    #12 0x7ff80437530f  (<unknown module>)

SUMMARY: AddressSanitizer: double-free (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x4b019) in wrap_free+0xa9
==51504==ABORTING
Abort trap: 6

pointer being freed was not allocated output

=================================================================
==51699==ERROR: AddressSanitizer: attempting free on address which was not malloc()-ed: 0x63100001480a in thread T0
    #0 0x116e93019 in wrap_free+0xa9 (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x4b019)
    #1 0x7ff80459585f in fclose+0x8b (libsystem_c.dylib:x86_64+0x2585f)
    #2 0x116e84a4f in wrap_fclose+0x8f (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x3ca4f)
    #3 0x1112df6d2 in _php_stream_free streams.c:475
    #4 0x1112f6609 in stream_resource_regular_dtor streams.c:1761
    #5 0x1117c4ddb in zend_resource_dtor zend_list.c:73
    #6 0x1117c709c in zend_close_rsrc_list zend_list.c:225
    #7 0x1115ce434 in zend_shutdown_executor_values zend_execute_API.c:277
    #8 0x1115d69ab in shutdown_executor zend_execute_API.c:416
    #9 0x1116af029 in zend_deactivate zend.c:1258
    #10 0x111223981 in php_request_shutdown main.c:1863
    #11 0x112637810 in do_cli php_cli.c:1135
    #12 0x11263173d in main php_cli.c:1333
    #13 0x7ff80437530f  (<unknown module>)

0x63100001480a is located 10 bytes inside of 65536-byte region [0x631000014800,0x631000024800)
allocated by thread T0 here:
    #0 0x116e92ed0 in wrap_malloc+0xa0 (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x4aed0)
    #1 0x11601795a in Curl_readwrite transfer.c:1120
    #2 0x116000b91 in multi_runsingle multi.c:2436
    #3 0x116000186 in curl_multi_perform multi.c:2714
    #4 0x10ed69e53 in zif_curl_multi_exec multi.c:225
    #5 0x111cfeda5 in ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER zend_vm_execute.h:1250
    #6 0x111870462 in execute_ex zend_vm_execute.h:55823
    #7 0x111871820 in zend_execute zend_vm_execute.h:60396
    #8 0x1116ba4c9 in zend_execute_scripts zend.c:1826
    #9 0x11122e18b in php_execute_script main.c:2542
    #10 0x11263544f in do_cli php_cli.c:964
    #11 0x11263173d in main php_cli.c:1333
    #12 0x7ff80437530f  (<unknown module>)

SUMMARY: AddressSanitizer: bad-free (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x4b019) in wrap_free+0xa9
==51699==ABORTING
Abort trap: 6

@lucasnetau
Copy link
Contributor Author

Alpine Linux produces a different error:

Thread 1 "php" received signal SIGSEGV, Segmentation fault.
0x000056407cf66486 in _php_stream_seek (stream=<error reading variable: Cannot access memory at address 0x7ffdf5351c68>, offset=<error reading variable: Cannot access memory at address 0x7ffdf5351c60>, whence=<error reading variable: Cannot access memory at address 0x7ffdf5351c5c>) at /usr/src/php/main/streams/streams.c:1295
1295	/usr/src/php/main/streams/streams.c: No such file or directory.
(gdb) bt
#0  0x000056407cf66486 in _php_stream_seek (stream=<error reading variable: Cannot access memory at address 0x7ffdf5351c68>, offset=<error reading variable: Cannot access memory at address 0x7ffdf5351c60>, whence=<error reading variable: Cannot access memory at address 0x7ffdf5351c5c>) at /usr/src/php/main/streams/streams.c:1295
#1  0x000056407cf69926 in stream_cookie_seeker (cookie=0x7ff82d077800, position=0x7ffdf53520d8, whence=1) at /usr/src/php/main/streams/cast.c:109
#2  0x00007ff82de108fc in ?? () from /lib/ld-musl-x86_64.so.1
#3  0x0000000000000000 in ?? ()

From Debian leaks are detected

=================================================================
==414==ERROR: LeakSanitizer: detected memory leaks

Indirect leak of 32480 byte(s) in 3 object(s) allocated from:
    #0 0x7f05b994ce8f in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x7f05b8fdc3c8  (/usr/lib/x86_64-linux-gnu/libcurl.so.4+0x2d3c8)

Indirect leak of 6440 byte(s) in 1 object(s) allocated from:
    #0 0x7f05b994d037 in __interceptor_calloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:154
    #1 0x7f05b8fd5d6f  (/usr/lib/x86_64-linux-gnu/libcurl.so.4+0x26d6f)

Indirect leak of 2736 byte(s) in 1 object(s) allocated from:
    #0 0x7f05b994d037 in __interceptor_calloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:154
    #1 0x7f05b8fd704f  (/usr/lib/x86_64-linux-gnu/libcurl.so.4+0x2804f)

Indirect leak of 416 byte(s) in 1 object(s) allocated from:
    #0 0x7f05b994d037 in __interceptor_calloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:154
    #1 0x7f05b8ff2ec8  (/usr/lib/x86_64-linux-gnu/libcurl.so.4+0x43ec8)
    #2 0x558e286c53dc in ZEND_DO_ICALL_SPEC_RETVAL_USED_HANDLER /usr/src/php/Zend/zend_vm_execute.h:1312
    #3 0x558e28966226 in execute_ex /usr/src/php/Zend/zend_vm_execute.h:56032
    #4 0x558e2898214d in zend_execute /usr/src/php/Zend/zend_vm_execute.h:60396
    #5 0x558e285a1598 in zend_execute_scripts /usr/src/php/Zend/zend.c:1826
    #6 0x558e282d8a32 in php_execute_script /usr/src/php/main/main.c:2542
    #7 0x558e28d39862 in do_cli /usr/src/php/sapi/cli/php_cli.c:964
    #8 0x558e28d3c2bb in main /usr/src/php/sapi/cli/php_cli.c:1333
    #9 0x7f05b83f4d09 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x23d09)

Indirect leak of 256 byte(s) in 4 object(s) allocated from:
    #0 0x7f05b994ce8f in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x7f05b8fc85ec  (/usr/lib/x86_64-linux-gnu/libcurl.so.4+0x195ec)

Indirect leak of 127 byte(s) in 2 object(s) allocated from:
    #0 0x7f05b994ce8f in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x7f05b8fdc4c1  (/usr/lib/x86_64-linux-gnu/libcurl.so.4+0x2d4c1)

Indirect leak of 100 byte(s) in 4 object(s) allocated from:
    #0 0x7f05b98fa817 in __interceptor_strdup ../../../../src/libsanitizer/asan/asan_interceptors.cpp:452
    #1 0x7f05b8fffba1  (/usr/lib/x86_64-linux-gnu/libcurl.so.4+0x50ba1)

Indirect leak of 96 byte(s) in 1 object(s) allocated from:
    #0 0x7f05b994d037 in __interceptor_calloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:154
    #1 0x7f05b8fd707a  (/usr/lib/x86_64-linux-gnu/libcurl.so.4+0x2807a)

Indirect leak of 70 byte(s) in 2 object(s) allocated from:
    #0 0x7f05b98fa817 in __interceptor_strdup ../../../../src/libsanitizer/asan/asan_interceptors.cpp:452
    #1 0x7f05b901f975  (/usr/lib/x86_64-linux-gnu/libcurl.so.4+0x70975)

Indirect leak of 48 byte(s) in 1 object(s) allocated from:
    #0 0x7f05b994ce8f in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x7f05b8fc2265  (/usr/lib/x86_64-linux-gnu/libcurl.so.4+0x13265)

Indirect leak of 30 byte(s) in 2 object(s) allocated from:
    #0 0x7f05b98fa817 in __interceptor_strdup ../../../../src/libsanitizer/asan/asan_interceptors.cpp:452
    #1 0x7f05b901f956  (/usr/lib/x86_64-linux-gnu/libcurl.so.4+0x70956)

Indirect leak of 24 byte(s) in 1 object(s) allocated from:
    #0 0x7f05b994d037 in __interceptor_calloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:154
    #1 0x7f05b8fdd1ef  (/usr/lib/x86_64-linux-gnu/libcurl.so.4+0x2e1ef)

Indirect leak of 16 byte(s) in 1 object(s) allocated from:
    #0 0x7f05b994d037 in __interceptor_calloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:154
    #1 0x7f05b8fc1276  (/usr/lib/x86_64-linux-gnu/libcurl.so.4+0x12276)

Indirect leak of 12 byte(s) in 1 object(s) allocated from:
    #0 0x7f05b98fa817 in __interceptor_strdup ../../../../src/libsanitizer/asan/asan_interceptors.cpp:452
    #1 0x7f05b8fda46a  (/usr/lib/x86_64-linux-gnu/libcurl.so.4+0x2b46a)

Indirect leak of 12 byte(s) in 1 object(s) allocated from:
    #0 0x7f05b98fa817 in __interceptor_strdup ../../../../src/libsanitizer/asan/asan_interceptors.cpp:452
    #1 0x7f05b8fd7d24  (/usr/lib/x86_64-linux-gnu/libcurl.so.4+0x28d24)

Indirect leak of 1 byte(s) in 1 object(s) allocated from:
    #0 0x7f05b98fa817 in __interceptor_strdup ../../../../src/libsanitizer/asan/asan_interceptors.cpp:452
    #1 0x7f05b8fd7de2  (/usr/lib/x86_64-linux-gnu/libcurl.so.4+0x28de2)

Indirect leak of 1 byte(s) in 1 object(s) allocated from:
    #0 0x7f05b98fa817 in __interceptor_strdup ../../../../src/libsanitizer/asan/asan_interceptors.cpp:452
    #1 0x7f05b8fd8366  (/usr/lib/x86_64-linux-gnu/libcurl.so.4+0x29366)

SUMMARY: AddressSanitizer: 42865 byte(s) leaked in 28 allocation(s).

@lucasnetau
Copy link
Contributor Author

Depending on where the out of memory error occurs it appears a different order of shutdown. With STREAM_DEBUG enabled.

No crash occurs when the Curl handle is closed first (DTOR CALLED before stream_free):

stream_alloc: STDIO:0x112a68100 persistent=(null)
stream_alloc: STDIO:0x112a68300 persistent=(null)
stream_alloc: STDIO:0x112a68500 persistent=(null)
stream_alloc: user-space:0x112a68700 persistent=(null)
stream_alloc: user-space:0x112a68800 persistent=(null)
*   Trying 34.193.132.77:80...
* Connected to httpbin.org (34.193.132.77) port 80 (#0)
> POST /post HTTP/1.1
Host: httpbin.org
Accept: */*
content-length: 100000000
user-agent:TestClient/1
Expect: 100-continue

PHP Fatal error:  Allowed memory size of 16777216 bytes exhausted at Zend/zend_string.h:249 (tried to allocate 7766032 bytes) in /tests/killcurl.php on line 75

Fatal error: Allowed memory size of 16777216 bytes exhausted at Zend/zend_string.h:249 (tried to allocate 7766032 bytes) in /tests/killcurl.php on line 75
* Closing connection 0
DTOR CALLED, ch = 12a6c000
stream_free: user-space:0x112a68800[fifo://113] in_free=0 opts=CALL_DTOR, RELEASE_STREAM, RSRC_DTOR
stream_free: user-space:0x112a68800[fifo://113] preserve_handle=0 release_cast=1 remove_rsrc=0
stream_free: user-space:0x112a68800[fifo://113] in_free=0 opts=CALL_DTOR, RELEASE_STREAM, RSRC_DTOR
stream_free: user-space:0x112a68800[fifo://113] preserve_handle=0 release_cast=1 remove_rsrc=0
closed stream reader
stream_free: user-space:0x112a68700[fifo://113] in_free=0 opts=CALL_DTOR, RELEASE_STREAM, RSRC_DTOR
stream_free: user-space:0x112a68700[fifo://113] preserve_handle=0 release_cast=1 remove_rsrc=0
closed stream writer
stream_free: STDIO:0x112a68500[php://stderr] in_free=0 opts=CALL_DTOR, RELEASE_STREAM, RSRC_DTOR
stream_free: STDIO:0x112a68500[php://stderr] preserve_handle=1 release_cast=0 remove_rsrc=0
stream_free: STDIO:0x112a68300[php://stdout] in_free=0 opts=CALL_DTOR, RELEASE_STREAM, RSRC_DTOR
stream_free: STDIO:0x112a68300[php://stdout] preserve_handle=1 release_cast=0 remove_rsrc=0
stream_free: STDIO:0x112a68100[php://stdin] in_free=0 opts=CALL_DTOR, RELEASE_STREAM, RSRC_DTOR
stream_free: STDIO:0x112a68100[php://stdin] preserve_handle=1 release_cast=0 remove_rsrc=0

Crash occurs if the input file stream is closed before the curl connection is closed (DTOR CALLED after stream_free)

stream_alloc: STDIO:0x110468100 persistent=(null)
stream_alloc: STDIO:0x110468300 persistent=(null)
stream_alloc: STDIO:0x110468500 persistent=(null)
stream_alloc: user-space:0x110468700 persistent=(null)
stream_alloc: user-space:0x110468800 persistent=(null)
*   Trying 34.193.132.77:80...
* Connected to httpbin.org (34.193.132.77) port 80 (#0)
> POST /post HTTP/1.1
Host: httpbin.org
Accept: */*
content-length: 100000000
user-agent:TestClient/1
Expect: 100-continue

< HTTP/1.1 100 Continue
PHP Fatal error:  Allowed memory size of 16777216 bytes exhausted at Zend/zend_string.h:152 (tried to allocate 6939768 bytes) in /tests/killcurl.php on line 57

Fatal error: Allowed memory size of 16777216 bytes exhausted at Zend/zend_string.h:152 (tried to allocate 6939768 bytes) in /tests/killcurl.php on line 57
stream_free: user-space:0x110468800[fifo://321] in_free=0 opts=CALL_DTOR, RELEASE_STREAM, RSRC_DTOR
stream_free: user-space:0x110468800[fifo://321] preserve_handle=0 release_cast=1 remove_rsrc=0
stream_free: user-space:0x110468800[fifo://321] in_free=0 opts=CALL_DTOR, RELEASE_STREAM, RSRC_DTOR
stream_free: user-space:0x110468800[fifo://321] preserve_handle=0 release_cast=1 remove_rsrc=0
closed stream reader
stream_free: user-space:0x110468700[fifo://321] in_free=0 opts=CALL_DTOR, RELEASE_STREAM, RSRC_DTOR
stream_free: user-space:0x110468700[fifo://321] preserve_handle=0 release_cast=1 remove_rsrc=0
closed stream writer
stream_free: STDIO:0x110468500[php://stderr] in_free=0 opts=CALL_DTOR, RELEASE_STREAM, RSRC_DTOR
stream_free: STDIO:0x110468500[php://stderr] preserve_handle=1 release_cast=0 remove_rsrc=0
stream_free: STDIO:0x110468300[php://stdout] in_free=0 opts=CALL_DTOR, RELEASE_STREAM, RSRC_DTOR
stream_free: STDIO:0x110468300[php://stdout] preserve_handle=1 release_cast=0 remove_rsrc=0
stream_free: STDIO:0x110468100[php://stdin] in_free=0 opts=CALL_DTOR, RELEASE_STREAM, RSRC_DTOR
stream_free: STDIO:0x110468100[php://stdin] preserve_handle=1 release_cast=0 remove_rsrc=0
DTOR CALLED, ch = 1046c000
=================================================================
==37427==ERROR: AddressSanitizer: attempting double-free on 0x631000014800 in thread T0:
...

@lucasnetau
Copy link
Contributor Author

@iluuu1994 please let me know if you need anything additional. I haven't been able to crash reliably on Linux after compiling with debug enabled (Alpine Linux SIGSEGV, Debian I get leaks but no crash). MacOS version crashes reliably every 2-3 invocations of the script with SIGABRT.

@lucasnetau
Copy link
Contributor Author

I haven't managed to get any further in working out what is going on. I did note that the release of the stream also generates very different debug logs between the shutdown and normally when calling fclose()

shutdown

stream_free: user-space:0x110468800[fifo://321] in_free=0 opts=CALL_DTOR, RELEASE_STREAM, RSRC_DTOR
stream_free: user-space:0x110468800[fifo://321] preserve_handle=0 release_cast=1 remove_rsrc=0
stream_free: user-space:0x110468800[fifo://321] in_free=0 opts=CALL_DTOR, RELEASE_STREAM, RSRC_DTOR
stream_free: user-space:0x110468800[fifo://321] preserve_handle=0 release_cast=1 remove_rsrc=0

fclose

stream_free: user-space:0x10e268800[fifo://842] in_free=0 opts=CALL_DTOR, RELEASE_STREAM
stream_free: user-space:0x10e268800[fifo://842] preserve_handle=0 release_cast=1 remove_rsrc=1
stream_free: user-space:0x10e268800[fifo://842] in_free=1 opts=CALL_DTOR, RELEASE_STREAM, RSRC_DTOR
stream_free: user-space:0x10e268800[fifo://842] in_free=0 opts=CALL_DTOR, RELEASE_STREAM, RSRC_DTOR
stream_free: user-space:0x10e268800[fifo://842] preserve_handle=0 release_cast=1 remove_rsrc=0

@iluuu1994
Copy link
Member

Hi @lucasnetau! Sorry, I've been a little busy the past few days. I'll look into this again soon.

@iluuu1994 iluuu1994 self-assigned this Apr 27, 2023
@iluuu1994
Copy link
Member

iluuu1994 commented May 11, 2023

I finally had a closer look. Unfortunately, I still cannot reproduce this issue locally. But like you say, this issue seems to be cause by zend_close_rsrc_list being called before zend_objects_store_free_object_storage. From my understanding, this is what happens:

  • We're creating a resource (fopen('fifo://' . ...)) which is added to EG(regular_list)
  • We're creating a CURL object, and passing the "raw" resource FILE*
  • When bailing (i.e. out-of-memory) the engine closes all resources in EG(regular_list), and only then frees objects
  • As part of object freeing, curl_easy_cleanup() is called which closes FILE* again

I think this can be avoided by calling curl_easy_cleanup() in dtor_obj instead of free_obj. At this point I'm not completely sure if this has other side-effects, like using global CURL objects from other dtors on shutdown that may now be closed. But that already sounds pretty dangerous. @lucasnetau Would it be possible to test the following patch?

Edit: On second thought, I'm not sure this changes anything, as the curl_easy_cleanup() won't actually remove the resource. This would work only if we kept a pointer to the resource around and close that, as this would actually remove the resource from EG(regular_list). Another option might be to just duplicate FILE* before passing it to CURL.

diff --git a/ext/curl/interface.c b/ext/curl/interface.c
index 2dd4a7bf65..7d3910625d 100644
--- a/ext/curl/interface.c
+++ b/ext/curl/interface.c
@@ -408,7 +408,7 @@ PHP_MINIT_FUNCTION(curl)
 
 	memcpy(&curl_object_handlers, &std_object_handlers, sizeof(zend_object_handlers));
 	curl_object_handlers.offset = XtOffsetOf(php_curl, std);
-	curl_object_handlers.free_obj = curl_free_obj;
+	curl_object_handlers.dtor_obj = curl_free_obj;
 	curl_object_handlers.get_gc = curl_get_gc;
 	curl_object_handlers.get_constructor = curl_get_constructor;
 	curl_object_handlers.clone_obj = curl_clone_obj;

The memory leaks should be addressed by GH-11231. It switches CURL to use emalloc which frees all allocated memory at the end of the request. This is because we use longjmp to abort the request on fatal errors, but this may also skip over corresponding free calls. There seem to be some issues in CI I'll have to look at.

@lucasnetau
Copy link
Contributor Author

@iluuu1994 good news is the patch above does appear to solve the double free issue, now I get a normal fatal error then exit.

I have only been able to replicate this on MacOS. I originally thought I crashed Linux but I haven't been able to replicate the SIGABRT (I was able to get a SIGSEV in Alpine linux)

One thing to note is that the piece of memory that cURL is freeing is not the *FILE but the upload buffer which takes data from the file. I had originally posted this to curl/curl#10964, somehow the two are linked but the cURL maintainers don't see how.

The bad new is the emalloc patches didn't fix the not allocated error, however after testing I saw you closed that PR:

==79532==ERROR: AddressSanitizer: attempting free on address which was not malloc()-ed: 0x000113eac000 in thread T0
    #0 0x10f9ce019 in wrap_free+0xa9 (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x4b019)
    #1 0x7ff80459585f in fclose+0x8b (libsystem_c.dylib:x86_64+0x2585f)
    #2 0x10f9bfa4f in wrap_fclose+0x8f (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x3ca4f)
    #3 0x109dfb9e2 in _php_stream_free streams.c:475
    #4 0x109e12919 in stream_resource_regular_dtor streams.c:1761
    #5 0x10a2e10eb in zend_resource_dtor zend_list.c:73
    #6 0x10a2e33ac in zend_close_rsrc_list zend_list.c:225
    #7 0x10a0ea744 in zend_shutdown_executor_values zend_execute_API.c:277
    #8 0x10a0f2cbb in shutdown_executor zend_execute_API.c:416
    #9 0x10a1cb339 in zend_deactivate zend.c:1258
    #10 0x109d3fc91 in php_request_shutdown main.c:1863
    #11 0x10b153b20 in do_cli php_cli.c:1135
    #12 0x10b14da4d in main php_cli.c:1333
    #13 0x7ff80437530f  (<unknown module>)

Address 0x000113eac000 is a wild pointer inside of access range of size 0x000000000001.
SUMMARY: AddressSanitizer: bad-free (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x4b019) in wrap_free+0xa9

@lucasnetau
Copy link
Contributor Author

As part of object freeing, curl_easy_cleanup() is called which closes FILE* again

curl_easy_cleanup shouldn't be doing that because in curl_multi_free_obj() calls _php_curl_verify_handlers on each cURL handle before freeing which takes care of un-assigning the FILE* that has gone away

And from what traces show it is the upload buffer being free'd that causes the double free.

@iluuu1994
Copy link
Member

iluuu1994 commented May 12, 2023

@lucasnetau Thanks for the hint! (I don't know the curl extension well) Now I'm no longer sure how this issue occurs at all... If zend_close_rsrc_list() always closes the FILE* first and marks the resource type as -1, then _php_curl_verify_handlers should always reset the ch->handlers.read->fp pointer . _php_curl_verify_handlers is also called in curl_free_obj so order shouldn't matter. It's hard to debug without being able to reproduce this.

Just to be sure, you don't have any other extensions enabled that might interact with the issue?

Edit: Oh, I missed the last part of your message (it is the upload buffer being free'd that causes the double free).

@lucasnetau
Copy link
Contributor Author

Nothing out of the ordinary in terms of extensions. PHP is installed via homebrew on MacOS.

Yes, the crash line in libcurl is Curl_safefree(data->state.ulbuf); and that appears to be allocated in libcurl

In terms of replicating, it does appear to be timing dependant on how far curl gets with the transfer before the OOM. You can try adjusting the 500 value to something smaller (say 100) or larger depending on your internet speed fwrite($writeStream, str_repeat('x', 500));

Since I can replicate, is there any steps you want me to do to capture any more information?

@iluuu1994
Copy link
Member

iluuu1994 commented May 13, 2023

@lucasnetau I whipped out my dusty old MacBook, I can reproduce the problem there. I'll have a look again soon.

@lucasnetau
Copy link
Contributor Author

Hi @iluuu1994 , were you ever about to find out anything more with this issue?

@iluuu1994
Copy link
Member

Hi @lucasnetau. No, I don't think I ever had another look. I'm not very familiar with curl or the extension. I would be grateful if somebody else could have a look, maybe @adoy?

@iluuu1994 iluuu1994 removed their assignment Jan 14, 2024
@Girgias
Copy link
Member

Girgias commented Jun 2, 2024

@SakiTakamachi could you possibly have a look at this as you have a macOS machine?

@nielsdos
Copy link
Member

nielsdos commented Jun 6, 2024

This reminds me of a mysqlnd bug with streams from some time ago, where the resource was freed before object destruction as well.
The solution there is to remove the resource from the regular list after opening the stream and doing the cleanup ourselves.

If this were reproducible on Linux I could give it a debug session, but it seems it's only reproducible on macOS seemingly? Do we know why?

@SakiTakamachi
Copy link
Member

At this time I don't know why this only happens on MacOS.

I will try to see if this can be reproduced in an Intel Linux environment.

@lucasnetau
Copy link
Contributor Author

I haven't tried but could it be a difference between Clang on MacOS and gcc on Linux?

@SakiTakamachi
Copy link
Member

Tried it on my Intel MacBook.

It is reproduced on Host OS (Mac OS).

% clang -v
Apple clang version 14.0.3 (clang-1403.0.22.14.1)
Target: x86_64-apple-darwin23.4.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

% curl -V
curl 8.4.0 (x86_64-apple-darwin23.0) libcurl/8.4.0 (SecureTransport) LibreSSL/3.3.6 zlib/1.2.12 nghttp2/1.58.0
Release-Date: 2023-10-11
Protocols: dict file ftp ftps gopher gophers http https imap imaps ldap ldaps mqtt pop3 pop3s rtsp smb smbs smtp smtps telnet tftp
Features: alt-svc AsynchDNS GSS-API HSTS HTTP2 HTTPS-proxy IPv6 Kerberos Largefile libz MultiSSL NTLM NTLM_WB SPNEGO SSL threadsafe UnixSockets

However, it does not reproduce on docker ubuntu on Intel MacBook. I've tried it with both gcc and clang (I also built curl from source and tried both patterns) but it doesn't reproduce.

# clang -v
Ubuntu clang version 14.0.0-1ubuntu1.1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/11
Selected GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/11
Candidate multilib: .;@m64
Selected multilib: .;@m64

# curl -V
curl 8.4.0 (x86_64-pc-linux-gnu) libcurl/8.4.0 OpenSSL/3.0.2 zlib/1.2.11 libpsl/0.21.0 (+libidn2/2.3.2)
Release-Date: 2023-10-11
Protocols: dict file ftp ftps gopher gophers http https imap imaps mqtt pop3 pop3s rtsp smb smbs smtp smtps telnet tftp
Features: alt-svc AsynchDNS HSTS HTTPS-proxy IPv6 Largefile libz NTLM PSL SSL threadsafe TLS-SRP UnixSockets

@SakiTakamachi
Copy link
Member

SakiTakamachi commented Jun 6, 2024

I forgot to write one thing. On my Intel MacBook I always end up with not allocated error. No double free errors occur.

edit: At Intel, I was building the master branch. M2 is 8.2. I will align the branches and test them tomorrow.

@nielsdos
Copy link
Member

nielsdos commented Jun 6, 2024

No need to keep looking for a Linux environment, I got my hands on an old mac system. From the Darwin version I see you're running Sonoma, so I'll give it a go tomorrow.

@SakiTakamachi
Copy link
Member

Thx!

I tested the master branch on Arm and got a double free error.

@nielsdos
Copy link
Member

nielsdos commented Jun 7, 2024

I reproduced this on macOS Ventura. And damn, this issue gave me a headache.
There's a lot to say here.
TL;DR: This issue is completely unrelated to Curl, and 100% related to streams. And in theory this also affects BSD OSes.

The analysis posted in this thread so far isn't right. Curl doesn't contain the code to fclose the FILE handle. In fact, Curl doesn't even receive the handle to begin with. What it gets is a pointer to our php_curl data structure and a function to callback to perform the read. This function is curl_read and it is part of PHP. That function uses the file pointer.

For the description below, I will use the FreeBSD libc source code because that's what macOS is based on and I couldn't find the sources of Apple's libc, but low and behold based on what I saw when stepping through gdb this is still the same code between macOS and FreeBSD.
EDIT: I found the sources of macOS and have updated this description.

Here's what actually happens:

  1. We're creating a FILE handle from a stream using the casting mechanism. This will create a cookie-based FILE handle using funopen.
  2. We're reading stream data using fread from the userspace stream. This will temporarily set a buffer into a field _bf.base here: https://github.com/apple-open-source-mirror/Libc/blob/5e566be7a7047360adfb35ffc44c6a019a854bea/stdio/FreeBSD/fread.c#L102-L103 This buffer is now equal to the upload buffer that Curl allocated and note that that buffer is owned by Curl.
  3. The fatal error occurs and we bail out from the fread function, notice how the reset code is never executed and so the buffer will still point to Curl's upload buffer instead of FILE's own buffer: https://github.com/apple-open-source-mirror/Libc/blob/5e566be7a7047360adfb35ffc44c6a019a854bea/stdio/FreeBSD/fread.c#L117
  4. The resources are destroyed, this includes our opened stream and because the FILE handle is cached, it gets destroyed as well. In fact, the stream code calls through fclose on purpose in this case.
  5. The fclose code frees the _bs.base buffer: https://github.com/apple-open-source-mirror/Libc/blob/5e566be7a7047360adfb35ffc44c6a019a854bea/stdio/FreeBSD/fclose.c#L66-L67 However, this is not the buffer that FILE owns but the one that Curl owns because it isn't reset properly due to the bailout!
  6. The objects are getting destroyed, and so the curl free logic is invoked. When Curl tries to gracefully clean up, it tries to free the buffer. But that buffer is actually already freed mistakingly by the C library!

This also explains why we can't reproduce it on Linux: this bizarre buffer swapperoo only happens on macOS and BSD, not on Linux.

You can easily prove this by testing this: https://gist.github.com/nielsdos/0d4f15f57ad984944249621e2670a50c

So why does it sometimes cause a use-after-free instead of double free? The reason you can change which kind of heap corruption triggers is because you're actually influencing the heap layout by setting an extra option, causing ASAN to draw different conclusions because the shadow bytes are slightly differently layed out.

As for the patch, Ilija's patch is not right: we should leave the object in a consistent state (e.g. the zend_object_std_dtor should definitely not be called in dtor_obj). Furthermore, when it bails out we will actually leak persistent memory because the dtor function is never called on fatal error. The fact that the dtor is not called is actually the reason the patch prevents the heap corruption, it just leaks instead. Also when native operating system resources (like actual file handles) are involved, those would be leaked as well. Also, that patch doesn't solve the fundamental problem that we're bailing out and not giving libc the chance to reset its state.

As an aside, I see there was a PR at one point to make Curl memory allocations use the request allocator, but it was closed because it isn't thread safe. I'd like to add that while you could enable it in ZTS, but nothing prevents third party extensions or libraries underlying to those extensions from using Curl too and storing that data for longer than a request. That would also cause problems. (That's the reason I can't easily switch libxml to the request allocator.)

Giving Curl a unique FILE handle might work, but this is probably not an intended use-case and when multiple close operations of different FILE handles for the same stream, this might not work properly. I do have a PoC patch for this though but this isn't the right solution. This must be solved at the stream layer.

Another possible temporary solution is using php_stream_read instead of fread in the Curl callback, but that doesn't solve the fundamental problem so the same issue will exist in other places as well...

@nielsdos
Copy link
Member

nielsdos commented Jun 7, 2024

A possible proper fix at the stream layer is this I think: https://gist.github.com/nielsdos/b47a42bb29f2ababdba35b37eaa3d468
It works on my system and shows no regressions. It simply disables buffering of that FILE handle in case a cookie-based FILE is used. That way, the buffer swapping will never happen and it avoids this bug. Since the stream layer itself also has a buffering mechanism this shouldn't hurt performance.

@SakiTakamachi @lucasnetau Can you please test the patch I linked? It should apply cleanly on master. I was only able to test on x86-64 macOS Ventura.

@SakiTakamachi
Copy link
Member

@nielsdos
Amazing! I tried your patch on both Intel and Arm, against the master branch. Both seem to solve the problem.

@lucasnetau
Copy link
Contributor Author

@nielsdos ,thank you very much for you investigation. I will test out the patch.

@lucasnetau
Copy link
Contributor Author

@nielsdos I can confirm that this patch fixes the issue for me on both Intel and ARM tested against 8.3 src

nielsdos added a commit to nielsdos/php-src that referenced this issue Jun 9, 2024
… allocated and malloc: double free for ptr errors

Although the issue was demonstrated using Curl, the issue is purely in
the streams layer of PHP.

Full analysis is written in phpGH-11078 [1], but here is the brief version:
Here's what actually happens:
1) We're creating a FILE handle from a stream using the casting mechanism.
   This will create a cookie-based FILE handle using funopen.
2) We're reading stream data using fread from the userspace stream. This will
   temporarily set a buffer into a field _bf.base [2]. This buffer is now equal
   to the upload buffer that Curl allocated and note that that buffer is owned
   by Curl.
3) The fatal error occurs and we bail out from the fread function, notice how
   the reset code is never executed and so the buffer will still point to
   Curl's upload buffer instead of FILE's own buffer [3].
4) The resources are destroyed, this includes our opened stream and because the
   FILE handle is cached, it gets destroyed as well.
   In fact, the stream code calls through fclose on purpose in this case.
5) The fclose code frees the _bs.base buffer [4].
   However, this is not the buffer that FILE owns but the one that Curl owns
   because it isn't reset properly due to the bailout!
6) The objects are getting destroyed, and so the curl free logic is invoked.
   When Curl tries to gracefully clean up, it tries to free the buffer.
   But that buffer is actually already freed mistakingly by the C library!

This also explains why we can't reproduce it on Linux: this bizarre buffer
swapping only happens on macOS and BSD, not on Linux.

To solve this, we switch to an unbuffered mode for cookie-based FILEs.
This avoids any stateful problems related to buffers especially when the
bailout mechanism triggers. As streams have their own buffering
mechanism, I don't expect this to impact performance.

[1] php#11078 (comment)
[2] https://github.com/apple-open-source-mirror/Libc/blob/5e566be7a7047360adfb35ffc44c6a019a854bea/stdio/FreeBSD/fread.c#L102-L103
[3] https://github.com/apple-open-source-mirror/Libc/blob/5e566be7a7047360adfb35ffc44c6a019a854bea/stdio/FreeBSD/fread.c#L117
[4] https://github.com/apple-open-source-mirror/Libc/blob/5e566be7a7047360adfb35ffc44c6a019a854bea/stdio/FreeBSD/fclose.c#L66-L67
@nielsdos
Copy link
Member

nielsdos commented Jun 9, 2024

Thanks, I've managed to create an isolated test and opened a PR, let's hope CI agrees too.

nielsdos added a commit to nielsdos/php-src that referenced this issue Jun 9, 2024
… allocated and malloc: double free for ptr errors

Although the issue was demonstrated using Curl, the issue is purely in
the streams layer of PHP.

Full analysis is written in phpGH-11078 [1], but here is the brief version:
Here's what actually happens:
1) We're creating a FILE handle from a stream using the casting mechanism.
   This will create a cookie-based FILE handle using funopen.
2) We're reading stream data using fread from the userspace stream. This will
   temporarily set a buffer into a field _bf.base [2]. This buffer is now equal
   to the upload buffer that Curl allocated and note that that buffer is owned
   by Curl.
3) The fatal error occurs and we bail out from the fread function, notice how
   the reset code is never executed and so the buffer will still point to
   Curl's upload buffer instead of FILE's own buffer [3].
4) The resources are destroyed, this includes our opened stream and because the
   FILE handle is cached, it gets destroyed as well.
   In fact, the stream code calls through fclose on purpose in this case.
5) The fclose code frees the _bs.base buffer [4].
   However, this is not the buffer that FILE owns but the one that Curl owns
   because it isn't reset properly due to the bailout!
6) The objects are getting destroyed, and so the curl free logic is invoked.
   When Curl tries to gracefully clean up, it tries to free the buffer.
   But that buffer is actually already freed mistakingly by the C library!

This also explains why we can't reproduce it on Linux: this bizarre buffer
swapping only happens on macOS and BSD, not on Linux.

To solve this, we switch to an unbuffered mode for cookie-based FILEs.
This avoids any stateful problems related to buffers especially when the
bailout mechanism triggers. As streams have their own buffering
mechanism, I don't expect this to impact performance.

[1] php#11078 (comment)
[2] https://github.com/apple-open-source-mirror/Libc/blob/5e566be7a7047360adfb35ffc44c6a019a854bea/stdio/FreeBSD/fread.c#L102-L103
[3] https://github.com/apple-open-source-mirror/Libc/blob/5e566be7a7047360adfb35ffc44c6a019a854bea/stdio/FreeBSD/fread.c#L117
[4] https://github.com/apple-open-source-mirror/Libc/blob/5e566be7a7047360adfb35ffc44c6a019a854bea/stdio/FreeBSD/fclose.c#L66-L67
nielsdos added a commit to nielsdos/php-src that referenced this issue Jun 9, 2024
… allocated and malloc: double free for ptr errors

Although the issue was demonstrated using Curl, the issue is purely in
the streams layer of PHP.

Full analysis is written in phpGH-11078 [1], but here is the brief version:
Here's what actually happens:
1) We're creating a FILE handle from a stream using the casting mechanism.
   This will create a cookie-based FILE handle using funopen.
2) We're reading stream data using fread from the userspace stream. This will
   temporarily set a buffer into a field _bf.base [2]. This buffer is now equal
   to the upload buffer that Curl allocated and note that that buffer is owned
   by Curl.
3) The fatal error occurs and we bail out from the fread function, notice how
   the reset code is never executed and so the buffer will still point to
   Curl's upload buffer instead of FILE's own buffer [3].
4) The resources are destroyed, this includes our opened stream and because the
   FILE handle is cached, it gets destroyed as well.
   In fact, the stream code calls through fclose on purpose in this case.
5) The fclose code frees the _bs.base buffer [4].
   However, this is not the buffer that FILE owns but the one that Curl owns
   because it isn't reset properly due to the bailout!
6) The objects are getting destroyed, and so the curl free logic is invoked.
   When Curl tries to gracefully clean up, it tries to free the buffer.
   But that buffer is actually already freed mistakingly by the C library!

This also explains why we can't reproduce it on Linux: this bizarre buffer
swapping only happens on macOS and BSD, not on Linux.

To solve this, we switch to an unbuffered mode for cookie-based FILEs.
This avoids any stateful problems related to buffers especially when the
bailout mechanism triggers. As streams have their own buffering
mechanism, I don't expect this to impact performance.

[1] php#11078 (comment)
[2] https://github.com/apple-open-source-mirror/Libc/blob/5e566be7a7047360adfb35ffc44c6a019a854bea/stdio/FreeBSD/fread.c#L102-L103
[3] https://github.com/apple-open-source-mirror/Libc/blob/5e566be7a7047360adfb35ffc44c6a019a854bea/stdio/FreeBSD/fread.c#L117
[4] https://github.com/apple-open-source-mirror/Libc/blob/5e566be7a7047360adfb35ffc44c6a019a854bea/stdio/FreeBSD/fclose.c#L66-L67
nielsdos added a commit to nielsdos/php-src that referenced this issue Jun 9, 2024
… allocated and malloc: double free for ptr errors

Although the issue was demonstrated using Curl, the issue is purely in
the streams layer of PHP.

Full analysis is written in phpGH-11078 [1], but here is the brief version:
Here's what actually happens:
1) We're creating a FILE handle from a stream using the casting mechanism.
   This will create a cookie-based FILE handle using funopen.
2) We're reading stream data using fread from the userspace stream. This will
   temporarily set a buffer into a field _bf.base [2]. This buffer is now equal
   to the upload buffer that Curl allocated and note that that buffer is owned
   by Curl.
3) The fatal error occurs and we bail out from the fread function, notice how
   the reset code is never executed and so the buffer will still point to
   Curl's upload buffer instead of FILE's own buffer [3].
4) The resources are destroyed, this includes our opened stream and because the
   FILE handle is cached, it gets destroyed as well.
   In fact, the stream code calls through fclose on purpose in this case.
5) The fclose code frees the _bs.base buffer [4].
   However, this is not the buffer that FILE owns but the one that Curl owns
   because it isn't reset properly due to the bailout!
6) The objects are getting destroyed, and so the curl free logic is invoked.
   When Curl tries to gracefully clean up, it tries to free the buffer.
   But that buffer is actually already freed mistakingly by the C library!

This also explains why we can't reproduce it on Linux: this bizarre buffer
swapping only happens on macOS and BSD, not on Linux.

To solve this, we switch to an unbuffered mode for cookie-based FILEs.
This avoids any stateful problems related to buffers especially when the
bailout mechanism triggers. As streams have their own buffering
mechanism, I don't expect this to impact performance.

[1] php#11078 (comment)
[2] https://github.com/apple-open-source-mirror/Libc/blob/5e566be7a7047360adfb35ffc44c6a019a854bea/stdio/FreeBSD/fread.c#L102-L103
[3] https://github.com/apple-open-source-mirror/Libc/blob/5e566be7a7047360adfb35ffc44c6a019a854bea/stdio/FreeBSD/fread.c#L117
[4] https://github.com/apple-open-source-mirror/Libc/blob/5e566be7a7047360adfb35ffc44c6a019a854bea/stdio/FreeBSD/fclose.c#L66-L67
@lucasnetau
Copy link
Contributor Author

In doing some benchmark testing with curl uploads via the stream I also discovered that this patch also fixed an issue I was seeing where cURL stopped uploading after stream_read() returned false on MacOS (Since returning an empty string causes cURL to think the upload is complete so you need to instead send a read error) [Note: I also use curl pause to stop and resume uploads as a flow control method if the buffer drains], now the upload works correctly even if the upload buffer gets drained.

In the testing I was able to do, cURL would resume however it never called the stream_read() leading to no data being transferred.

Performance wise, I don't see an variance between old and patched version POSTing a large file to a localhost server, both sat at around 1350 MB/s throughput

nielsdos added a commit that referenced this issue Jun 10, 2024
* PHP-8.2:
  Fix GH-11078: PHP Fatal error triggers pointer being freed was not allocated and malloc: double free for ptr errors
nielsdos added a commit that referenced this issue Jun 10, 2024
* PHP-8.3:
  Fix GH-11078: PHP Fatal error triggers pointer being freed was not allocated and malloc: double free for ptr errors
@nielsdos
Copy link
Member

@lucasnetau Thanks for the results, fix will be in the next release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
7 participants