Closed lucasnetau closed 3 months ago
Might be a duplicate of #10670
Might be a duplicate of https://github.com/php/php-src/issues/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.
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?
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.
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
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).
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:
...
@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.
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
Hi @lucasnetau! Sorry, I've been a little busy the past few days. I'll look into this again soon.
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:
fopen('fifo://' . ...)
) which is added to EG(regular_list)
FILE*
EG(regular_list)
, and only then frees objectscurl_easy_cleanup()
is called which closes FILE*
againI 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.
@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 https://github.com/curl/curl/issues/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
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.
@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).
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?
@lucasnetau I whipped out my dusty old MacBook, I can reproduce the problem there. I'll have a look again soon.
Hi @iluuu1994 , were you ever about to find out anything more with this issue?
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?
@SakiTakamachi could you possibly have a look at this as you have a macOS machine?
I'll try it some more, but I can't reproduce it on either Intel or Arm MacBooks...
Hi @SakiTakamachi , I can still reliably cause this crash every 2-3 executions of the test script.
Fatal error: Allowed memory size of 16777216 bytes exhausted (tried to allocate 11109840 bytes) in /tests/killcurl.php on line 57
php(30218,0x7ff8505be700) malloc: double free for ptr 0x7fb4c0040000
php(30218,0x7ff8505be700) malloc: *** set a breakpoint in malloc_error_break to debug
Abort trap: 6
It is very timing dependant (ensuring the OOM occurs in the stream handler) and also bandwidth dependant.
This line can be adjusted, reducing 500 to a say 100 if you have a faster connection. I believe from memory you need to see the HTTP/1.1 100 Continue output before the error triggers.
fwrite($writeStream, str_repeat('x', 500));
@lucasnetau Thanks, in my environment I was able to reproduce it by setting it to 1000. And so far this problem only occurs on my Arm processor. Is your environment also Arm?
This was done on x86_64, pretty sure I also replicated on m1 arm however the results above are from Intel
No matter how many times I try, I can't reproduce it on Intel...There may be other factors involved, but for now, I'll check the details on M2, which I was able to reproduce.
There has been little progress since I have just set up a "debug environment that can be reproduced," but when I measured the number of loops, in my environment, two patterns always occur a crash around 2500 times and a crash around 11000 times.
The former is pointer being freed was not allocated, and the latter is double free. There is a correlation, at least in my environment, between the types of errors that occur and the number of times the loop runs before crashing.
It depends on which line the OOM occurs, it needs to occur in the stream_read() which would make sense since that would be when cURL is reading data, stream_write would be on the PHP side.
If the crash is on line 57, which is within stream_read() I get the double free
self::$buffers[$this->bufferKey] = substr(self::$buffers[$this->bufferKey], $written);
If I comment out this line to no set the header then I get pointer being freed was not allocated if it crashed in stream_read
curl_setopt($curl, CURLOPT_HTTPHEADER, ["content-length: 100000000","user-agent:TestClient/1",'Transfer-Encoding:']);
//->to
//curl_setopt($curl, CURLOPT_HTTPHEADER, ["content-length: 100000000","user-agent:TestClient/1",'Transfer-Encoding:']);
If it's on line 75 within stream_write() I only get an OOM, no crash
self::$buffers[$this->bufferKey] .= $data;
@SakiTakamachi a more succinct reproducer based on the observation about the crash in stream_read. Only implemented the required stream functions and only opened a RO stream which with trigger an OOM when read.
For me you can change the constant DOUBLE_FREE from true to false to trigger the two different errors.
Have tested on both Intel and M1 ARM MacOS, same output on both.
<?php declare(strict_types=1);
/**
* true = malloc: double free for ptr
* false = malloc: *** error for object 0x: pointer being freed was not allocated
*/
const DOUBLE_FREE = true;
const MEM = 8388608;
ini_set('memory_limit', (string)MEM);
class CrashingFifo {
/** @var resource|null Stream context (this is set by PHP) */
public $context;
function stream_open($path, $mode, $options, &$opened_path): bool
{
return true;
}
function stream_read(int $count): false|string|null
{
return str_repeat('x', MEM+1);
}
}
stream_register_wrapper('fifo', CrashingFifo::class);
$readStream = fopen('fifo://1', 'r');
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => 'http://httpbin.org/post',
CURLOPT_PUT => true,
CURLOPT_INFILE => $readStream,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_VERBOSE => true,
]);
if (DOUBLE_FREE) {
curl_setopt($curl, CURLOPT_HTTPHEADER,
["content-length: " . MEM, "user-agent:TestClient/1", 'Transfer-Encoding:']);
}
$multi = curl_multi_init();
curl_multi_add_handle($multi, $curl);
do {
curl_multi_exec($multi, $still_running);
} while ($still_running);
Cool, reproducibility is perfect.
I've researched this issue, but I still don't have any idea what's causing it. Ilija's idea solves the double free problem, but I don't understand why curl tries to release the memory allocated on the stream side in the first place. I'm wondering if this strange behavior is also related to the unassigned error.
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?
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.
I haven't tried but could it be a difference between Clang on MacOS and gcc on Linux?
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
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.
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.
Thx!
I tested the master branch on Arm and got a double free error.
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...
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.
@nielsdos Amazing! I tried your patch on both Intel and Arm, against the master branch. Both seem to solve the problem.
@nielsdos ,thank you very much for you investigation. I will test out the patch.
@nielsdos I can confirm that this patch fixes the issue for me on both Intel and ARM tested against 8.3 src
Thanks, I've managed to create an isolated test and opened a PR, let's hope CI agrees too.
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
@lucasnetau Thanks for the results, fix will be in the next release.
Description
The following code:
https://gist.github.com/lucasnetau/244ba31f321307e06177f48582273e86
Resulted in this output:
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:
Pointer being freed was not allocated:
Double free was reported to curl via https://github.com/curl/curl/issues/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