diff --git a/NEWS b/NEWS index cfb82c4f0ca2..db0366165674 100644 --- a/NEWS +++ b/NEWS @@ -12,6 +12,10 @@ PHP NEWS - Sockets: . Fixed various memory related issues in ext/sockets. (David Carlier) +- Standard: + . Fixed bug GH-22845 (Use-after-free when a user filter stashes its bucket + brigade resource). (iliaal) + 30 Jul 2026, PHP 8.4.24 - Calendar: diff --git a/ext/standard/tests/filters/gh22845.phpt b/ext/standard/tests/filters/gh22845.phpt new file mode 100644 index 000000000000..5c1242603a1f --- /dev/null +++ b/ext/standard/tests/filters/gh22845.phpt @@ -0,0 +1,40 @@ +--TEST-- +GH-22845 (Use-after-free when a user filter stashes its bucket brigade resource) +--FILE-- +data); + stream_bucket_append($out, $bucket); + } + return PSFS_PASS_ON; + } +} +stream_filter_register('stash', 'stash_filter'); + +$fp = fopen('php://memory', 'r+'); +fwrite($fp, "hello world"); +rewind($fp); +stream_filter_append($fp, 'stash', STREAM_FILTER_READ); +var_dump(fread($fp, 100)); + +// The brigades lived on the filter caller's stack. Both stashed resources must be +// closed now that the callback has returned, not left pointing at freed stack. +foreach (['stash_in', 'stash_out'] as $name) { + try { + stream_bucket_make_writeable($GLOBALS[$name]); + echo "$name: no error\n"; + } catch (\Throwable $e) { + echo "$name: ", $e::class, "\n"; + } +} +echo "done\n"; +?> +--EXPECT-- +string(11) "hello world" +stash_in: TypeError +stash_out: TypeError +done diff --git a/ext/standard/tests/filters/gh22845_bailout.phpt b/ext/standard/tests/filters/gh22845_bailout.phpt new file mode 100644 index 000000000000..f8c0809e87b4 --- /dev/null +++ b/ext/standard/tests/filters/gh22845_bailout.phpt @@ -0,0 +1,39 @@ +--TEST-- +GH-22845 (user filter bucket brigade resource is closed when the callback bails out) +--EXTENSIONS-- +zend_test +--SKIPIF-- + +--FILE-- +data); + stream_bucket_append($out, $b); + } + register_shutdown_function(function() { + try { + stream_bucket_make_writeable($GLOBALS['stash_in']); + echo "shutdown: no error\n"; + } catch (\Throwable $e) { + echo "shutdown: ", $e::class, "\n"; + } + }); + zend_trigger_bailout(); + } +} +stream_filter_register('bf', 'bf'); + +$fp = fopen('php://memory', 'r+'); +fwrite($fp, "hello world"); +rewind($fp); +stream_filter_append($fp, 'bf', STREAM_FILTER_READ); +fread($fp, 100); +?> +--EXPECTF-- +Fatal error: Bailout in %s on line %d +shutdown: TypeError diff --git a/ext/standard/user_filters.c b/ext/standard/user_filters.c index 83b1986b82a3..8b58b37dce85 100644 --- a/ext/standard/user_filters.c +++ b/ext/standard/user_filters.c @@ -137,7 +137,8 @@ static php_stream_filter_status_t userfilter_filter( zval func_name; zval retval; zval args[4]; - int call_result; + int call_result = FAILURE; + bool bailout = false; /* the userfilter object probably doesn't exist anymore */ if (CG(unclean_shutdown)) { @@ -188,27 +189,33 @@ static php_stream_filter_status_t userfilter_filter( ZVAL_BOOL(&args[3], flags & PSFS_FLAG_FLUSH_CLOSE); - call_result = call_user_function(NULL, - obj, - &func_name, - &retval, - 4, args); + zend_try { + call_result = call_user_function(NULL, + obj, + &func_name, + &retval, + 4, args); + } zend_catch { + bailout = true; + } zend_end_try(); zval_ptr_dtor(&func_name); - if (call_result == SUCCESS && Z_TYPE(retval) != IS_UNDEF) { - convert_to_long(&retval); - ret = (int)Z_LVAL(retval); - } else if (call_result == FAILURE) { - php_error_docref(NULL, E_WARNING, "Failed to call filter function"); - } + if (!bailout) { + if (call_result == SUCCESS && Z_TYPE(retval) != IS_UNDEF) { + convert_to_long(&retval); + ret = (int)Z_LVAL(retval); + } else if (call_result == FAILURE) { + php_error_docref(NULL, E_WARNING, "Failed to call filter function"); + } - if (bytes_consumed) { - *bytes_consumed = zval_get_long(&args[2]); - } + if (bytes_consumed) { + *bytes_consumed = zval_get_long(&args[2]); + } - if (buckets_in->head) { - php_error_docref(NULL, E_WARNING, "Unprocessed filter buckets remaining on input brigade"); + if (buckets_in->head) { + php_error_docref(NULL, E_WARNING, "Unprocessed filter buckets remaining on input brigade"); + } } /* filter resources are cleaned up by the stream destructor, @@ -223,6 +230,9 @@ static php_stream_filter_status_t userfilter_filter( zend_string_release(stream_name); + zend_list_close(Z_RES(args[1])); + zend_list_close(Z_RES(args[0])); + zval_ptr_dtor(&args[3]); zval_ptr_dtor(&args[2]); zval_ptr_dtor(&args[1]); @@ -231,6 +241,10 @@ static php_stream_filter_status_t userfilter_filter( stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE; stream->flags |= orig_no_fclose; + if (bailout) { + zend_bailout(); + } + return ret; }