You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Hash table use-after-destroy in function table during stream wrapper cleanup (Zend/zend_hash.c) #20286
#0 __pthread_kill_implementation (threadid=<optimized out>, signo=6, no_tid=0) at ./nptl/pthread_kill.c:44
#1 __pthread_kill_internal (threadid=<optimized out>, signo=6) at ./nptl/pthread_kill.c:89
#2 __GI___pthread_kill (threadid=<optimized out>, signo=signo@entry=6) at ./nptl/pthread_kill.c:100
#3 0x00007ffff6845e2e in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#4 0x00007ffff6828888 in __GI_abort () at ./stdlib/abort.c:77
#5 0x00007ffff68287f0 in __assert_fail_base (fmt=<optimized out>, assertion=<optimized out>, file=<optimized out>, line=<optimized out>, function=<optimized out>) at ./assert/assert.c:118
#6 0x000055555e1e8edd in _zend_is_inconsistent (ht=0x7bfff2890048, file=0x55555eae7c61 "/home/dcarlier/Contribs/php-src/Zend/zend_hash.c", line=2681) at /home/dcarlier/Contribs/php-src/Zend/zend_hash.c:73
#7 0x000055555e230965 in zend_hash_find (ht=0x7bfff2890048, key=0x7bfff28890a0) at /home/dcarlier/Contribs/php-src/Zend/zend_hash.c:2681
#8 0x000055555d164b9f in zend_is_callable_check_func (callable=0x7bfff3b0e020, frame=0x0, fcc=0x7bfff3b0e040, strict_class=false, error=0x0, suppress_deprecation=true)
at /home/dcarlier/Contribs/php-src/Zend/zend_API.c:3940
#9 0x000055555d15e751 in zend_is_callable_at_frame (callable=0x7bfff3b0e020, object=0x7bfff2802000, frame=0x0, check_flags=2, fcc=0x7bfff3b0e040, error=0x0)
at /home/dcarlier/Contribs/php-src/Zend/zend_API.c:4160
#10 0x000055555d16c077 in zend_is_callable_ex (callable=0x7bfff3b0e020, object=0x7bfff2802000, check_flags=2, callable_name=0x0, fcc=0x7bfff3b0e040, error=0x0)
at /home/dcarlier/Contribs/php-src/Zend/zend_API.c:4244
#11 0x000055555d421436 in zend_call_method_if_exists (object=0x7bfff2802000, method_name=0x7bfff28890a0, retval=0x7bfff39526e0, param_count=0, params=0x0)
at /home/dcarlier/Contribs/php-src/Zend/zend_execute_API.c:1140
#12 0x000055555cc88e19 in php_userstreamop_close (stream=0x7bfff2884600, close_handle=1) at /home/dcarlier/Contribs/php-src/main/streams/userspace.c:699
#13 0x000055555cc5515b in _php_stream_free (stream=0x7bfff2884600, close_options=11) at /home/dcarlier/Contribs/php-src/main/streams/streams.c:480
#14 0x000055555cc6eb86 in stream_resource_regular_dtor (rsrc=0x7bfff3a84aa0) at /home/dcarlier/Contribs/php-src/main/streams/streams.c:1856
#15 0x000055555e4fa54f in zend_resource_dtor (res=0x7bfff2801cc0) at /home/dcarlier/Contribs/php-src/Zend/zend_list.c:73
#16 0x000055555e4fb70d in list_entry_destructor (zv=0x7bfff39526a0) at /home/dcarlier/Contribs/php-src/Zend/zend_list.c:180
#17 0x000055555e213d6f in _zend_hash_packed_del_val (ht=0x7e8ff4de88d0, idx=8, zv=0x7bfff288a808) at /home/dcarlier/Contribs/php-src/Zend/zend_hash.c:1467
#18 0x000055555e22710c in zend_hash_graceful_reverse_destroy (ht=0x7e8ff4de88d0) at /home/dcarlier/Contribs/php-src/Zend/zend_hash.c:2036
#19 0x000055555e4fcd15 in zend_destroy_rsrc_list (ht=0x7e8ff4de88d0) at /home/dcarlier/Contribs/php-src/Zend/zend_list.c:244
#20 0x000055555e67a0cd in zend_deactivate () at /home/dcarlier/Contribs/php-src/Zend/zend.c:1361
#21 0x000055555cb45343 in php_request_shutdown (dummy=0x0) at /home/dcarlier/Contribs/php-src/main/main.c:2020
#22 0x000055555e6a022e in do_cli (argc=2, argv=0x7c2ff4de1bd0) at /home/dcarlier/Contribs/php-src/sapi/cli/php_cli.c:1158
#23 0x000055555e697952 in main (argc=2, argv=0x7c2ff4de1bd0) at /home/dcarlier/Contribs/php-src/sapi/cli/php_cli.c:1362
Ouch! This code causes the stream_close call to execute so late that EG(active) is false already! This should be disallowed. zend_call_function & friends prevent this by checking EG(active), but zend_call_method_if_exists doesn't.
This issue should only be triggerable via resources, so perhaps just fixing this in php_userstreamop_close instead of adding this extra bloat to the Zend API is enough.
Related to #19849. Normally, resources are closed in zend_close_rsrc_list() before shutting down the executor. However, in this example, another resource is opened when closing the resources, which will be left unclosed. zend_destroy_rsrc_list(&EG(regular_list)); (in zend_deactivate()) will then attempt to close it again, which includes the call to userland after the executor has shut down.
We could simply keep closing the resources again in zend_close_rsrc_list(), but there's some concern for infinite recursion. This is not an isolated incidence (there have been at least a handful similar issues), so IMO skipping function calls in zend_call_method_if_exists() when EG(active) is false is reasonable.
There are similar considerations for objects: Destructors can create new objects, that need to be destroyed. For objects we chose to keep calling destructors.
The advantage is that it doesn't add an exception to the rule "object destructors are always called".
This does loop infinitely if destructors keep creating new objects (until growing the object store exceeds the memory limit), but doesn't appear to be an issue in practice. Programs that trigger this behavior are broken anyway.
I'm in favor of continuing to close the resources in zend_close_rsrc_list() while new ones are created, unless the chances of infinite loops are higher in the case of resources.
Okay, if you prefer this solution it works for me. Regardless, it may be a good idea to add the check to zend_call_method_if_exists() so we can avoid triggering callbacks when the VM is shut down. We can keep a ZEND_UNREACHABLE() in debug builds so such cases can still be caught through fuzzing.
I agree with Arnaud that we should keep closing in zend_close_rsrc_list until the list is empty.
I still think we should add an assert to zend_call_method_if_exists as well though, at least.
Activity
devnexen commentedon Oct 25, 2025
ndossche commentedon Oct 25, 2025
Ouch! This code causes the stream_close call to execute so late that
EG(active)is false already! This should be disallowed.zend_call_function& friends prevent this by checkingEG(active), butzend_call_method_if_existsdoesn't.This issue should only be triggerable via resources, so perhaps just fixing this in
php_userstreamop_closeinstead of adding this extra bloat to the Zend API is enough.devnexen commentedon Oct 27, 2025
would something like this works ?
let me know if you prefer to work on it yourself though.
ndossche commentedon Oct 27, 2025
I haven't deeply looked into it. But that seems to go in the right direction although I don't immediately know why you'd need to NULL the wrapper
devnexen commentedon Oct 27, 2025
Because it will attempt to destroy the resource otherwise, leading to UB.
iluuu1994 commentedon Oct 28, 2025
Related to #19849. Normally, resources are closed in
zend_close_rsrc_list()before shutting down the executor. However, in this example, another resource is opened when closing the resources, which will be left unclosed.zend_destroy_rsrc_list(&EG(regular_list));(inzend_deactivate()) will then attempt to close it again, which includes the call to userland after the executor has shut down.We could simply keep closing the resources again in
zend_close_rsrc_list(), but there's some concern for infinite recursion. This is not an isolated incidence (there have been at least a handful similar issues), so IMO skipping function calls inzend_call_method_if_exists()whenEG(active)isfalseis reasonable.arnaud-lb commentedon Oct 30, 2025
There are similar considerations for objects: Destructors can create new objects, that need to be destroyed. For objects we chose to keep calling destructors.
The advantage is that it doesn't add an exception to the rule "object destructors are always called".
This does loop infinitely if destructors keep creating new objects (until growing the object store exceeds the memory limit), but doesn't appear to be an issue in practice. Programs that trigger this behavior are broken anyway.
I'm in favor of continuing to close the resources in
zend_close_rsrc_list()while new ones are created, unless the chances of infinite loops are higher in the case of resources.iluuu1994 commentedon Oct 30, 2025
Okay, if you prefer this solution it works for me. Regardless, it may be a good idea to add the check to
zend_call_method_if_exists()so we can avoid triggering callbacks when the VM is shut down. We can keep aZEND_UNREACHABLE()in debug builds so such cases can still be caught through fuzzing.ndossche commentedon Oct 30, 2025
I agree with Arnaud that we should keep closing in
zend_close_rsrc_listuntil the list is empty.I still think we should add an assert to
zend_call_method_if_existsas well though, at least.Fix phpGH-20286: use-after-destroy during userland stream_close()
Fix GH-20286: use-after-destroy during userland stream_close()
Merge branch 'PHP-8.3' into PHP-8.4
Merge branch 'PHP-8.4' into PHP-8.5
Merge branch 'PHP-8.5'