[php-src] Issue #10072: PHP crashes while cleaning up stream resources when execute_ex is overridden and a __call trampoline is used

From: Date: Fri, 09 Dec 2022 15:10:58 +0000
Subject: [php-src] Issue #10072: PHP crashes while cleaning up stream resources when execute_ex is overridden and a __call trampoline is used
Groups: php.bugs 
Request: Send a blank email to php-bugs+get-243086@lists.php.net to get a copy of this message
Issue: https://github.com/php/php-src/issues/10072 Author: derickr ### Description This is reproducible by first installing [this extension](https://github.com/derickr/extension-skeleton), which pretty much only sets up a dummy overload of [zend_execute_ex](https://github.com/derickr/extension-skeleton/blob/master/skeleton.c#L46-L58). Then run the code below (by @Girgias), with the following command: ``` USE_ZEND_ALLOC=0 gdb --args php -n -dextension=skeleton trampoline.php ``` ```php <?php class DummyStreamWrapper { /** @var resource|null */ public $context; /** @var resource|null */ public $handle; public function stream_cast(int $castAs) { return $this->handle; } public function stream_close(): void { } public function stream_open(string $path, string $mode, int $options = 0, ?string &$openedPath = null): bool { return true; } public function stream_read(int $count) { return 0; } public function stream_seek(int $offset, int $whence = SEEK_SET): bool { return true; } public function stream_set_option(int $option, int $arg1, ?int $arg2): bool { return false; } public function stream_stat() { return []; } public function stream_tell() { return []; } public function stream_truncate(int $newSize): bool { return true; } public function stream_write(string $data) { } public function unlink(string $path): bool { return false; } } class TrampolineTest { /** @var resource|null */ public $context; /** @var object|null */ private $wrapper; public function __call(string $name, array $arguments) { if (!$this->wrapper) { $this->wrapper = new DummyStreamWrapper(); } echo 'Trampoline for ', $name, PHP_EOL; return $this->wrapper->$name(...$arguments); } } stream_wrapper_register('custom', TrampolineTest::class); $fp = fopen("custom://myvar", "r+"); ?> ``` This crashes with the following back trace: ``` Program received signal SIGSEGV, Segmentation fault. 0x0000555555c9d51a in ZEND_CALL_TRAMPOLINE_SPEC_OBSERVER_HANDLER () at /home/derick/dev/php/php-src.git/Zend/zend_vm_execute.h:3459 3459 LOAD_OPLINE(); (gdb) bt #0 0x0000555555c9d51a in ZEND_CALL_TRAMPOLINE_SPEC_OBSERVER_HANDLER () at /home/derick/dev/php/php-src.git/Zend/zend_vm_execute.h:3459 #1 0x0000555555d09234 in execute_ex (ex=0x555556d01de0) at /home/derick/dev/php/php-src.git/Zend/zend_vm_execute.h:55911 #2 0x00007ffff36521df in skeleton_execute_ex (execute_data=0x555556d01de0) at /home/derick/dev/php/extension-skeleton/skeleton.c:48 #3 0x0000555555c43c5c in zend_call_function (fci=0x7fffffffb9f0, fci_cache=0x7fffffffb860) at /home/derick/dev/php/php-src.git/Zend/zend_execute_API.c:912 #4 0x0000555555c43168 in _call_user_function_impl (object=0x555556d5ea48, function_name=0x7fffffffba80, retval_ptr=0x7fffffffba70, param_count=0, params=0x0, named_params=0x0) at /home/derick/dev/php/php-src.git/Zend/zend_execute_API.c:712 #5 0x0000555555be95ca in php_userstreamop_close (stream=0x555556d5c850, close_handle=1) at /home/derick/dev/php/php-src.git/main/streams/userspace.c:708 #6 0x0000555555bdb464 in _php_stream_free (stream=0x555556d5c850, close_options=11) at /home/derick/dev/php/php-src.git/main/streams/streams.c:475 #7 0x0000555555bde057 in stream_resource_regular_dtor (rsrc=0x7fffffffbb40) at /home/derick/dev/php/php-src.git/main/streams/streams.c:1666 #8 0x0000555555c77cb6 in zend_resource_dtor (res=0x555556d5be60) at /home/derick/dev/php/php-src.git/Zend/zend_list.c:73 #9 0x0000555555c781ac in zend_close_rsrc_list (ht=0x555556a2fd30 <executor_globals+560>) at /home/derick/dev/php/php-src.git/Zend/zend_list.c:224 #10 0x0000555555c41b03 in zend_shutdown_executor_values (fast_shutdown=false) at /home/derick/dev/php/php-src.git/Zend/zend_execute_API.c:270 #11 0x0000555555c42405 in shutdown_executor () at /home/derick/dev/php/php-src.git/Zend/zend_execute_API.c:403 #12 0x0000555555c5a12c in zend_deactivate () at /home/derick/dev/php/php-src.git/Zend/zend.c:1271 #13 0x0000555555bbe61f in php_request_shutdown (dummy=0x0) at /home/derick/dev/php/php-src.git/main/main.c:1847 #14 0x0000555555dc5be7 in do_cli (argc=4, argv=0x555556a639c0) at /home/derick/dev/php/php-src.git/sapi/cli/php_cli.c:1135 #15 0x0000555555dc631f in main (argc=4, argv=0x555556a639c0) at /home/derick/dev/php/php-src.git/sapi/cli/php_cli.c:1367 ``` What seems to happen here is that the LOAD_OPLINE tries to access opline from prev_execute_data (line 3458 sets execute_data to prev_execute_data, which is NULL in this case. The code in zend_vm_def.h is: ``` 3450 if (EXPECTED(zend_execute_ex == execute_ex)) { 3451 LOAD_OPLINE_EX(); 3452 SAVE_OPLINE(); 3453 zend_observer_fcall_begin(execute_data); 3454 ZEND_VM_ENTER_EX(); 3455 } else { 3456 SAVE_OPLINE_EX(); 3457 zend_observer_fcall_begin(execute_data); 3458 execute_data = EX(prev_execute_data); 3459 LOAD_OPLINE(); 3460 ZEND_ADD_CALL_FLAG(call, ZEND_CALL_TOP); 3461 zend_execute_ex(call); 3462 } ``` I guess that there is no prev_execute_data, as this is called when cleaning up the stream resource, which isn't run from user land. If you change line 3450 (and regenerate the executor with Zend/zend_vm_gen.php) to: ``` 3450 if (EXPECTED(zend_execute_ex == execute_ex) || !EX(prev_execute_data)) { 3451 LOAD_OPLINE_EX(); 3452 SAVE_OPLINE(); 3453 zend_observer_fcall_begin(execute_data); 3454 ZEND_VM_ENTER_EX(); 3455 } else { ``` Then the crash is gone, but the overloaded zend_execute_ex is not run. In Xdebug, that results in the __call line for stream_close to be missing in a function trace. You can see that in this sample, where the __call is there for stream_open, but not for stream_close. ``` TRACE START [2022-12-09 11:25:02.275874] 0.0005 0 -> {main}() /tmp/issue2139/mine/trampoline.php:0 0.0005 0 -> stream_wrapper_register($protocol = 'custom', $class = 'TrampolineTest') /tmp/issue2139/mine/trampoline.php:99 0.0006 0 -> fopen($filename = 'custom://myvar', $mode = 'r+') /tmp/issue2139/mine/trampoline.php:102 0.0006 0 -> TrampolineTest->stream_open('custom://myvar', 'r+', 0, NULL) /tmp/issue2139/mine/trampoline.php:102 0.0006 0 -> TrampolineTest->__call($name = 'stream_open', $arguments = [0 => 'custom://myvar', 1 => 'r+', 2 => 0, 3 => NULL]) /tmp/issue2139/mine/trampoline.php:102 0.0007 0 -> DummyStreamWrapper->stream_open($path = 'custom://myvar', $mode = 'r+', $options = 0, $openedPath = NULL) /tmp/issue2139/mine/trampoline.php:87 0.0009 0 -> TrampolineTest->stream_close() /tmp/issue2139/mine/trampoline.php:0 0.0009 0 -> DummyStreamWrapper->stream_close() /tmp/issue2139/mine/trampoline.php:87 0.0010 0 TRACE END [2022-12-09 11:25:02.276503] ``` So although the one line patch fixes the crash, it does not fix the issue. I haven't figured out what the *correct* fix is. ### PHP Version PHP 8.1.15-dev, but also reproducible with PHP 8.2.1-dev ### Operating System Debian unstable, but it is not relevant

« previous php.bugs (#243086) next »