From 8d702cad7e1dbf22a89990738e25e044a804ec95 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Tue, 24 Jun 2025 13:49:34 -0700 Subject: [PATCH 01/15] Deprecate returning non-string values from a user output handler https://wiki.php.net/rfc/deprecations_php_8_4 --- UPGRADING | 6 +++++ main/output.c | 39 ++++++++++++++++++++-------- tests/output/bug60768.phpt | 2 +- tests/output/ob_start_basic_002.phpt | 8 ++++-- 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/UPGRADING b/UPGRADING index 5491d85f529d8..b8c82ff841087 100644 --- a/UPGRADING +++ b/UPGRADING @@ -259,6 +259,12 @@ PHP 8.5 UPGRADE NOTES 4. Deprecated Functionality ======================================== +- Core: + . Returning a non-string from a user output handler is deprecated. The + deprecation warning will bypass user output handlers to ensure it is + visible. + RFC: https://wiki.php.net/rfc/deprecations_php_8_4 + - Hash: . The MHASH_* constants have been deprecated. These have been overlooked when the mhash*() function family has been deprecated per diff --git a/main/output.c b/main/output.c index ef6be672d1c16..827a94b0ab10b 100644 --- a/main/output.c +++ b/main/output.c @@ -948,6 +948,7 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl if (handler->flags & PHP_OUTPUT_HANDLER_USER) { zval ob_args[2]; zval retval; + ZVAL_UNDEF(&retval); /* ob_data */ ZVAL_STRINGL(&ob_args[0], handler->buffer.data, handler->buffer.used); @@ -959,17 +960,33 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl handler->func.user->fci.params = ob_args; handler->func.user->fci.retval = &retval; -#define PHP_OUTPUT_USER_SUCCESS(retval) ((Z_TYPE(retval) != IS_UNDEF) && !(Z_TYPE(retval) == IS_FALSE)) - if (SUCCESS == zend_call_function(&handler->func.user->fci, &handler->func.user->fcc) && PHP_OUTPUT_USER_SUCCESS(retval)) { - /* user handler may have returned TRUE */ - status = PHP_OUTPUT_HANDLER_NO_DATA; - if (Z_TYPE(retval) != IS_FALSE && Z_TYPE(retval) != IS_TRUE) { - convert_to_string(&retval); - if (Z_STRLEN(retval)) { - context->out.data = estrndup(Z_STRVAL(retval), Z_STRLEN(retval)); - context->out.used = Z_STRLEN(retval); - context->out.free = 1; - status = PHP_OUTPUT_HANDLER_SUCCESS; + if (SUCCESS == zend_call_function(&handler->func.user->fci, &handler->func.user->fcc) && Z_TYPE(retval) != IS_UNDEF) { + if (Z_TYPE(retval) != IS_STRING) { + // Make sure that we don't get lost in an output buffer + int old_flags = OG(flags); + OG(flags) = old_flags & (~PHP_OUTPUT_ACTIVATED); + php_error_docref( + NULL, + E_DEPRECATED, + "Returning a non-string result from user output handler %s is deprecated", + ZSTR_VAL(handler->name) + ); + OG(flags) = old_flags; + } + if (Z_TYPE(retval) == IS_FALSE) { + /* call failed, pass internal buffer along */ + status = PHP_OUTPUT_HANDLER_FAILURE; + } else { + /* user handler may have returned TRUE */ + status = PHP_OUTPUT_HANDLER_NO_DATA; + if (Z_TYPE(retval) != IS_FALSE && Z_TYPE(retval) != IS_TRUE) { + convert_to_string(&retval); + if (Z_STRLEN(retval)) { + context->out.data = estrndup(Z_STRVAL(retval), Z_STRLEN(retval)); + context->out.used = Z_STRLEN(retval); + context->out.free = 1; + status = PHP_OUTPUT_HANDLER_SUCCESS; + } } } } else { diff --git a/tests/output/bug60768.phpt b/tests/output/bug60768.phpt index 4de7fbd69ff4b..1ab702fa64681 100644 --- a/tests/output/bug60768.phpt +++ b/tests/output/bug60768.phpt @@ -5,7 +5,7 @@ Bug #60768 Output buffer not discarded global $storage; -ob_start(function($buffer) use (&$storage) { $storage .= $buffer; }, 20); +ob_start(function($buffer) use (&$storage) { $storage .= $buffer; return ''; }, 20); echo str_repeat("0", 20); // fill in the buffer diff --git a/tests/output/ob_start_basic_002.phpt b/tests/output/ob_start_basic_002.phpt index 700dab5d3c381..7207c2a959149 100644 --- a/tests/output/ob_start_basic_002.phpt +++ b/tests/output/ob_start_basic_002.phpt @@ -35,19 +35,23 @@ foreach ($callbacks as $callback) { } ?> ---EXPECT-- +--EXPECTF-- --> Use callback 'return_empty_string': --> Use callback 'return_false': My output. +Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_false is deprecated in %s on line %d + --> Use callback 'return_null': + +Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_null is deprecated in %s on line %d --> Use callback 'return_string': I stole your output. --> Use callback 'return_zero': 0 - +Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_zero is deprecated in %s on line %d From d213952d6409939dcc91ae5c88ce63f0cc9a40f7 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Tue, 24 Jun 2025 14:35:46 -0700 Subject: [PATCH 02/15] Only disable the problematic output handler And update tests --- Zend/tests/concat/bug79836.phpt | 7 ++++++- Zend/tests/concat/bug79836_1.phpt | 5 ++++- Zend/tests/concat/bug79836_2.phpt | 5 ++++- Zend/tests/declare/gh18033_2.phpt | 3 ++- Zend/tests/gh11189.phpt | 2 ++ Zend/tests/gh11189_1.phpt | 2 ++ Zend/tests/gh16408.phpt | 7 ++++++- ext/session/tests/user_session_module/bug61728.phpt | 3 ++- main/output.c | 8 ++++---- sapi/cli/tests/gh8827-002.phpt | 12 +++++++++++- tests/output/ob_start_basic_002.phpt | 9 +++++---- 11 files changed, 48 insertions(+), 15 deletions(-) diff --git a/Zend/tests/concat/bug79836.phpt b/Zend/tests/concat/bug79836.phpt index 5fb07396762f5..0383bd0fb6368 100644 --- a/Zend/tests/concat/bug79836.phpt +++ b/Zend/tests/concat/bug79836.phpt @@ -14,5 +14,10 @@ $c .= []; ob_end_clean(); echo $counter . "\n"; ?> ---EXPECT-- +--EXPECTF-- +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d + +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d + +Deprecated: ob_end_clean(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d 3 diff --git a/Zend/tests/concat/bug79836_1.phpt b/Zend/tests/concat/bug79836_1.phpt index 86e7f47671849..28a04ad47f5cc 100644 --- a/Zend/tests/concat/bug79836_1.phpt +++ b/Zend/tests/concat/bug79836_1.phpt @@ -14,5 +14,8 @@ $x = $c . $x; ob_end_clean(); echo "Done\n"; ?> ---EXPECT-- +--EXPECTF-- +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d + +Deprecated: ob_end_clean(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d Done diff --git a/Zend/tests/concat/bug79836_2.phpt b/Zend/tests/concat/bug79836_2.phpt index b02fcc13ea11b..eeba0135468cd 100644 --- a/Zend/tests/concat/bug79836_2.phpt +++ b/Zend/tests/concat/bug79836_2.phpt @@ -21,5 +21,8 @@ $x = $c . $xxx; ob_end_clean(); echo $x . "\n"; ?> ---EXPECT-- +--EXPECTF-- +Deprecated: X::__toString(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d + +Deprecated: ob_end_clean(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabc diff --git a/Zend/tests/declare/gh18033_2.phpt b/Zend/tests/declare/gh18033_2.phpt index 8fdcff1b51e6c..45d506b9b1d87 100644 --- a/Zend/tests/declare/gh18033_2.phpt +++ b/Zend/tests/declare/gh18033_2.phpt @@ -13,4 +13,5 @@ ob_start(function() { ); }); ?> ---EXPECT-- +--EXPECTF-- +Deprecated: PHP Request Shutdown: Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d diff --git a/Zend/tests/gh11189.phpt b/Zend/tests/gh11189.phpt index adbc3ce487c95..e7a2ee66625df 100644 --- a/Zend/tests/gh11189.phpt +++ b/Zend/tests/gh11189.phpt @@ -29,4 +29,6 @@ while (1) { ?> --EXPECTF-- Success +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d + Fatal error: Allowed memory size of %s bytes exhausted%s(tried to allocate %s bytes) in %s on line %d diff --git a/Zend/tests/gh11189_1.phpt b/Zend/tests/gh11189_1.phpt index 17b9967bc3182..a0891c1c99c55 100644 --- a/Zend/tests/gh11189_1.phpt +++ b/Zend/tests/gh11189_1.phpt @@ -29,4 +29,6 @@ while (1) { ?> --EXPECTF-- Success +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d + Fatal error: Allowed memory size of %s bytes exhausted%s(tried to allocate %s bytes) in %s on line %d diff --git a/Zend/tests/gh16408.phpt b/Zend/tests/gh16408.phpt index f28c6435bfe73..de09c84803dea 100644 --- a/Zend/tests/gh16408.phpt +++ b/Zend/tests/gh16408.phpt @@ -12,5 +12,10 @@ $c .= []; ob_end_clean(); echo $counter . "\n"; ?> ---EXPECT-- +--EXPECTF-- +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d + +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d + +Deprecated: ob_end_clean(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d 3 diff --git a/ext/session/tests/user_session_module/bug61728.phpt b/ext/session/tests/user_session_module/bug61728.phpt index fd79fa6b2bce4..7bc042e0116fa 100644 --- a/ext/session/tests/user_session_module/bug61728.phpt +++ b/ext/session/tests/user_session_module/bug61728.phpt @@ -40,5 +40,6 @@ class MySessionHandler implements SessionHandlerInterface { session_set_save_handler(new MySessionHandler()); session_start(); ?> ---EXPECT-- +--EXPECTF-- +Deprecated: ob_end_flush(): Returning a non-string result from user output handler output_html is deprecated in %s on line %d 8 diff --git a/main/output.c b/main/output.c index 827a94b0ab10b..edf3952d856f8 100644 --- a/main/output.c +++ b/main/output.c @@ -962,16 +962,16 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl if (SUCCESS == zend_call_function(&handler->func.user->fci, &handler->func.user->fcc) && Z_TYPE(retval) != IS_UNDEF) { if (Z_TYPE(retval) != IS_STRING) { - // Make sure that we don't get lost in an output buffer - int old_flags = OG(flags); - OG(flags) = old_flags & (~PHP_OUTPUT_ACTIVATED); + // Make sure that we don't get lost in the current output buffer + // by disabling it + handler->flags |= PHP_OUTPUT_HANDLER_DISABLED; php_error_docref( NULL, E_DEPRECATED, "Returning a non-string result from user output handler %s is deprecated", ZSTR_VAL(handler->name) ); - OG(flags) = old_flags; + handler->flags &= (~PHP_OUTPUT_HANDLER_DISABLED); } if (Z_TYPE(retval) == IS_FALSE) { /* call failed, pass internal buffer along */ diff --git a/sapi/cli/tests/gh8827-002.phpt b/sapi/cli/tests/gh8827-002.phpt index 00fd5cfa78f74..72e2aeb077c3f 100644 --- a/sapi/cli/tests/gh8827-002.phpt +++ b/sapi/cli/tests/gh8827-002.phpt @@ -32,10 +32,20 @@ print "STDOUT:\n"; fclose(STDOUT); var_dump(@fopen('php://stdout', 'a')); ?> ---EXPECT-- +--EXPECTF-- STDIN: + +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d bool(false) + +Deprecated: var_dump(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d STDERR: + +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d bool(false) + +Deprecated: var_dump(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d STDOUT: + +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d bool(false) diff --git a/tests/output/ob_start_basic_002.phpt b/tests/output/ob_start_basic_002.phpt index 7207c2a959149..e9af2b5e1904c 100644 --- a/tests/output/ob_start_basic_002.phpt +++ b/tests/output/ob_start_basic_002.phpt @@ -40,18 +40,19 @@ foreach ($callbacks as $callback) { --> Use callback 'return_false': -My output. -Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_false is deprecated in %s on line %d +Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_false is deprecated in %s on line %d +My output. --> Use callback 'return_null': +Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_null is deprecated in %s on line %d -Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_null is deprecated in %s on line %d --> Use callback 'return_string': I stole your output. --> Use callback 'return_zero': -0 + Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_zero is deprecated in %s on line %d +0 From 25c118318629e911101286b2557dcc334203f793 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Tue, 24 Jun 2025 15:05:30 -0700 Subject: [PATCH 03/15] Tests --- sapi/cli/tests/gh8827-003.phpt | 1 + .../exception_handler.phpt | 91 +++++++++++++++++ .../exception_handler_nested.phpt | 97 +++++++++++++++++++ .../multiple_handlers.phpt | 89 +++++++++++++++++ 4 files changed, 278 insertions(+) create mode 100644 tests/output/ob_start_callback_bad_return/exception_handler.phpt create mode 100644 tests/output/ob_start_callback_bad_return/exception_handler_nested.phpt create mode 100644 tests/output/ob_start_callback_bad_return/multiple_handlers.phpt diff --git a/sapi/cli/tests/gh8827-003.phpt b/sapi/cli/tests/gh8827-003.phpt index 11f7880770ed9..12b6798a57c27 100644 --- a/sapi/cli/tests/gh8827-003.phpt +++ b/sapi/cli/tests/gh8827-003.phpt @@ -34,6 +34,7 @@ file_put_contents('php://fd/2', "Goes to stderrFile\n"); ob_start(function ($buffer) use ($stdoutStream) { fwrite($stdoutStream, $buffer); + return ''; }, 1); print "stdoutFile:\n"; diff --git a/tests/output/ob_start_callback_bad_return/exception_handler.phpt b/tests/output/ob_start_callback_bad_return/exception_handler.phpt new file mode 100644 index 0000000000000..ee176d8afa697 --- /dev/null +++ b/tests/output/ob_start_callback_bad_return/exception_handler.phpt @@ -0,0 +1,91 @@ +--TEST-- +ob_start(): Check behaviour with deprecation converted to exception +--FILE-- +>>"; + return null; +} + +function return_false($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return false; +} + +function return_true($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return true; +} + +function return_zero($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return 0; +} + +$cases = ['return_null', 'return_false', 'return_true', 'return_zero']; +foreach ($cases as $case) { + $log = []; + echo "\n\nTesting: $case\n"; + ob_start($case); + echo "Inside of $case"; + try { + ob_end_flush(); + } catch (\ErrorException $e) { + echo $e . "\n"; + } + echo "\nEnd of $case, log was:\n"; + echo implode("\n", $log); +} + +?> +--EXPECTF-- +Testing: return_null +ErrorException: ob_end_flush(): Returning a non-string result from user output handler return_null is deprecated in %s:%d +Stack trace: +#0 [internal function]: {closure:%s:%d}(8192, 'ob_end_flush():...', %s, %d) +#1 %s(%d): ob_end_flush() +#2 {main} + +End of return_null, log was: +return_null: <<>> + +Testing: return_false +Inside of return_falseErrorException: ob_end_flush(): Returning a non-string result from user output handler return_false is deprecated in %s:%d +Stack trace: +#0 [internal function]: {closure:%s:%d}(8192, 'ob_end_flush():...', %s, %d) +#1 %s(%d): ob_end_flush() +#2 {main} + +End of return_false, log was: +return_false: <<>> + +Testing: return_true +ErrorException: ob_end_flush(): Returning a non-string result from user output handler return_true is deprecated in %s:%d +Stack trace: +#0 [internal function]: {closure:%s:%d}(8192, 'ob_end_flush():...', %s, %d) +#1 %s(%d): ob_end_flush() +#2 {main} + +End of return_true, log was: +return_true: <<>> + +Testing: return_zero +0ErrorException: ob_end_flush(): Returning a non-string result from user output handler return_zero is deprecated in %s:%d +Stack trace: +#0 [internal function]: {closure:%s:%d}(8192, 'ob_end_flush():...', %s, %d) +#1 %s(%d): ob_end_flush() +#2 {main} + +End of return_zero, log was: +return_zero: <<>> diff --git a/tests/output/ob_start_callback_bad_return/exception_handler_nested.phpt b/tests/output/ob_start_callback_bad_return/exception_handler_nested.phpt new file mode 100644 index 0000000000000..5983c1a626aee --- /dev/null +++ b/tests/output/ob_start_callback_bad_return/exception_handler_nested.phpt @@ -0,0 +1,97 @@ +--TEST-- +ob_start(): Check behaviour with deprecation converted to exception +--FILE-- +>>"; + return null; +} + +function return_false($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return false; +} + +function return_true($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return true; +} + +function return_zero($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return 0; +} + +ob_start('return_null'); +ob_start('return_false'); +ob_start('return_true'); +ob_start('return_zero'); + +echo "In all of them\n\n"; +try { + ob_end_flush(); +} catch (\ErrorException $e) { + echo $e->getMessage() . "\n"; +} +echo "Ended return_zero handler\n\n"; + +try { + ob_end_flush(); +} catch (\ErrorException $e) { + echo $e->getMessage() . "\n"; +} +echo "Ended return_true handler\n\n"; + +try { + ob_end_flush(); +} catch (\ErrorException $e) { + echo $e->getMessage() . "\n"; +} +echo "Ended return_false handler\n\n"; + +try { + ob_end_flush(); +} catch (\ErrorException $e) { + echo $e->getMessage() . "\n"; +} +echo "Ended return_null handler\n\n"; + +echo "All handlers are over\n\n"; +echo implode("\n", $log); + +?> +--EXPECT-- +ob_end_flush(): Returning a non-string result from user output handler return_null is deprecated +Ended return_null handler + +All handlers are over + +return_zero: <<>> +return_true: <<<0ob_end_flush(): Returning a non-string result from user output handler return_zero is deprecated +Ended return_zero handler + +>>> +return_false: <<>> +return_null: <<>> diff --git a/tests/output/ob_start_callback_bad_return/multiple_handlers.phpt b/tests/output/ob_start_callback_bad_return/multiple_handlers.phpt new file mode 100644 index 0000000000000..33ae03f9e5589 --- /dev/null +++ b/tests/output/ob_start_callback_bad_return/multiple_handlers.phpt @@ -0,0 +1,89 @@ +--TEST-- +ob_start(): Check behaviour with multiple nested handlers with had return values +--FILE-- +>>"; + return $string; +} + +function return_empty_string($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return ""; +} + +function return_false($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return false; +} + +function return_true($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return true; +} + +function return_null($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return null; +} + +function return_string($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return "I stole your output."; +} + +function return_zero($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return 0; +} + +ob_start('return_given_string'); +ob_start('return_empty_string'); +ob_start('return_false'); +ob_start('return_true'); +ob_start('return_null'); +ob_start('return_string'); +ob_start('return_zero'); + +echo "Testing..."; + +ob_end_flush(); +ob_end_flush(); +ob_end_flush(); +ob_end_flush(); +ob_end_flush(); +ob_end_flush(); +ob_end_flush(); + +echo "\n\nLog:\n"; +echo implode("\n", $log); +?> +--EXPECTF-- +Log: +return_zero: <<>> +return_string: <<< +Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_zero is deprecated in %s on line %d +0>>> +return_null: <<>> +return_true: <<< +Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_null is deprecated in %s on line %d +>>> +return_false: <<< +Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_true is deprecated in %s on line %d +>>> +return_empty_string: <<< +Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_false is deprecated in %s on line %d + +Deprecated: ob_end_flush(): Returning a non-string result from user output handler return_true is deprecated in %s on line %d +>>> +return_given_string: <<<>>> From cb1026aaf111b44e2da3b92c452f89a40f376a5f Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Tue, 24 Jun 2025 15:27:32 -0700 Subject: [PATCH 04/15] upgrading --- UPGRADING | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/UPGRADING b/UPGRADING index b8c82ff841087..f35a987ad9da7 100644 --- a/UPGRADING +++ b/UPGRADING @@ -261,8 +261,9 @@ PHP 8.5 UPGRADE NOTES - Core: . Returning a non-string from a user output handler is deprecated. The - deprecation warning will bypass user output handlers to ensure it is - visible. + deprecation warning will bypass the handler with the bad return to ensure + it is visible; if there are nested output handlers the next one will still + be used. RFC: https://wiki.php.net/rfc/deprecations_php_8_4 - Hash: From 52f5e47f10c4f489d64ab8d0f1babf08a8c45fe4 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Thu, 26 Jun 2025 13:06:50 -0700 Subject: [PATCH 05/15] Handlers might be freed from OOM --- main/output.c | 61 +++++++++++++++---- .../exception_handler.phpt | 19 +++--- .../handler_removed.phpt | 21 +++++++ 3 files changed, 83 insertions(+), 18 deletions(-) create mode 100644 tests/output/ob_start_callback_bad_return/handler_removed.phpt diff --git a/main/output.c b/main/output.c index edf3952d856f8..2d52bf521cd12 100644 --- a/main/output.c +++ b/main/output.c @@ -934,6 +934,7 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl return PHP_OUTPUT_HANDLER_FAILURE; } + bool still_have_handler = true; /* storable? */ if (php_output_handler_append(handler, &context->in) && !context->op) { context->op = original_op; @@ -971,7 +972,22 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl "Returning a non-string result from user output handler %s is deprecated", ZSTR_VAL(handler->name) ); - handler->flags &= (~PHP_OUTPUT_HANDLER_DISABLED); + // Check if the handler is still in the list of handlers to + // determine if the PHP_OUTPUT_HANDLER_DISABLED flag can + // be removed + still_have_handler = false; + int handler_count = php_output_get_level(); + if (handler_count) { + php_output_handler **handlers = (php_output_handler **) zend_stack_base(&OG(handlers)); + for (int iii = 0; iii < handler_count; ++iii) { + php_output_handler *curr_handler = handlers[iii]; + if (curr_handler == handler) { + handler->flags &= (~PHP_OUTPUT_HANDLER_DISABLED); + still_have_handler = true; + break; + } + } + } } if (Z_TYPE(retval) == IS_FALSE) { /* call failed, pass internal buffer along */ @@ -1013,14 +1029,18 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl status = PHP_OUTPUT_HANDLER_FAILURE; } } - handler->flags |= PHP_OUTPUT_HANDLER_STARTED; + if (still_have_handler) { + handler->flags |= PHP_OUTPUT_HANDLER_STARTED; + } OG(running) = NULL; } switch (status) { case PHP_OUTPUT_HANDLER_FAILURE: - /* disable this handler */ - handler->flags |= PHP_OUTPUT_HANDLER_DISABLED; + if (still_have_handler) { + /* disable this handler */ + handler->flags |= PHP_OUTPUT_HANDLER_DISABLED; + } /* discard any output */ if (context->out.data && context->out.free) { efree(context->out.data); @@ -1029,18 +1049,22 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl context->out.data = handler->buffer.data; context->out.used = handler->buffer.used; context->out.free = 1; - handler->buffer.data = NULL; - handler->buffer.used = 0; - handler->buffer.size = 0; + if (still_have_handler) { + handler->buffer.data = NULL; + handler->buffer.used = 0; + handler->buffer.size = 0; + } break; case PHP_OUTPUT_HANDLER_NO_DATA: /* handler ate all */ php_output_context_reset(context); ZEND_FALLTHROUGH; case PHP_OUTPUT_HANDLER_SUCCESS: - /* no more buffered data */ - handler->buffer.used = 0; - handler->flags |= PHP_OUTPUT_HANDLER_PROCESSED; + if (still_have_handler) { + /* no more buffered data */ + handler->buffer.used = 0; + handler->flags |= PHP_OUTPUT_HANDLER_PROCESSED; + } break; } @@ -1242,6 +1266,19 @@ static int php_output_stack_pop(int flags) } php_output_handler_op(orphan, &context); } + // If it isn't still in the stack, cannot free it + bool still_have_handler = false; + int handler_count = php_output_get_level(); + if (handler_count) { + php_output_handler **handlers = (php_output_handler **) zend_stack_base(&OG(handlers)); + for (int iii = 0; iii < handler_count; ++iii) { + php_output_handler *curr_handler = handlers[iii]; + if (curr_handler == orphan) { + still_have_handler = true; + break; + } + } + } /* pop it off the stack */ zend_stack_del_top(&OG(handlers)); @@ -1257,7 +1294,9 @@ static int php_output_stack_pop(int flags) } /* destroy the handler (after write!) */ - php_output_handler_free(&orphan); + if (still_have_handler) { + php_output_handler_free(&orphan); + } php_output_context_dtor(&context); return 1; diff --git a/tests/output/ob_start_callback_bad_return/exception_handler.phpt b/tests/output/ob_start_callback_bad_return/exception_handler.phpt index ee176d8afa697..413f855d40cc4 100644 --- a/tests/output/ob_start_callback_bad_return/exception_handler.phpt +++ b/tests/output/ob_start_callback_bad_return/exception_handler.phpt @@ -38,9 +38,9 @@ foreach ($cases as $case) { $log = []; echo "\n\nTesting: $case\n"; ob_start($case); - echo "Inside of $case"; + echo "Inside of $case\n"; try { - ob_end_flush(); + ob_end_flush(); } catch (\ErrorException $e) { echo $e . "\n"; } @@ -58,17 +58,20 @@ Stack trace: #2 {main} End of return_null, log was: -return_null: <<>> +return_null: <<>> Testing: return_false -Inside of return_falseErrorException: ob_end_flush(): Returning a non-string result from user output handler return_false is deprecated in %s:%d +Inside of return_false +ErrorException: ob_end_flush(): Returning a non-string result from user output handler return_false is deprecated in %s:%d Stack trace: #0 [internal function]: {closure:%s:%d}(8192, 'ob_end_flush():...', %s, %d) #1 %s(%d): ob_end_flush() #2 {main} End of return_false, log was: -return_false: <<>> +return_false: <<>> Testing: return_true ErrorException: ob_end_flush(): Returning a non-string result from user output handler return_true is deprecated in %s:%d @@ -78,7 +81,8 @@ Stack trace: #2 {main} End of return_true, log was: -return_true: <<>> +return_true: <<>> Testing: return_zero 0ErrorException: ob_end_flush(): Returning a non-string result from user output handler return_zero is deprecated in %s:%d @@ -88,4 +92,5 @@ Stack trace: #2 {main} End of return_zero, log was: -return_zero: <<>> +return_zero: <<>> diff --git a/tests/output/ob_start_callback_bad_return/handler_removed.phpt b/tests/output/ob_start_callback_bad_return/handler_removed.phpt new file mode 100644 index 0000000000000..f26dec04182de --- /dev/null +++ b/tests/output/ob_start_callback_bad_return/handler_removed.phpt @@ -0,0 +1,21 @@ +--TEST-- +ob_start(): Check behaviour with deprecation when OOM triggers handler removal +--FILE-- + +--EXPECTF-- +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d + +Fatal error: Allowed memory size of %d bytes exhausted at %s:%d (tried to allocate %d bytes) in %s on line %d From 578157166f843e4c1ce473629487c23b21f8b41f Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Thu, 26 Jun 2025 13:30:59 -0700 Subject: [PATCH 06/15] Location not always given --- tests/output/ob_start_callback_bad_return/handler_removed.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/output/ob_start_callback_bad_return/handler_removed.phpt b/tests/output/ob_start_callback_bad_return/handler_removed.phpt index f26dec04182de..c524ed04029f4 100644 --- a/tests/output/ob_start_callback_bad_return/handler_removed.phpt +++ b/tests/output/ob_start_callback_bad_return/handler_removed.phpt @@ -18,4 +18,4 @@ while (1) { --EXPECTF-- Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d -Fatal error: Allowed memory size of %d bytes exhausted at %s:%d (tried to allocate %d bytes) in %s on line %d +Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d From 470b7e990ae91fb35fb4e3e935aad944fc866d8b Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Thu, 26 Jun 2025 13:36:13 -0700 Subject: [PATCH 07/15] context is also unavailable --- main/output.c | 27 +++++++++---------- ...emoved.phpt => handler_false_removed.phpt} | 2 +- .../handler_true_removed.phpt | 21 +++++++++++++++ .../handler_zero_removed.phpt | 21 +++++++++++++++ 4 files changed, 56 insertions(+), 15 deletions(-) rename tests/output/ob_start_callback_bad_return/{handler_removed.phpt => handler_false_removed.phpt} (92%) create mode 100644 tests/output/ob_start_callback_bad_return/handler_true_removed.phpt create mode 100644 tests/output/ob_start_callback_bad_return/handler_zero_removed.phpt diff --git a/main/output.c b/main/output.c index 2d52bf521cd12..1437eb37273ba 100644 --- a/main/output.c +++ b/main/output.c @@ -1035,12 +1035,15 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl OG(running) = NULL; } + if (!still_have_handler) { + // Handler and context will have both already been freed + return status; + } + switch (status) { case PHP_OUTPUT_HANDLER_FAILURE: - if (still_have_handler) { - /* disable this handler */ - handler->flags |= PHP_OUTPUT_HANDLER_DISABLED; - } + /* disable this handler */ + handler->flags |= PHP_OUTPUT_HANDLER_DISABLED; /* discard any output */ if (context->out.data && context->out.free) { efree(context->out.data); @@ -1049,22 +1052,18 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl context->out.data = handler->buffer.data; context->out.used = handler->buffer.used; context->out.free = 1; - if (still_have_handler) { - handler->buffer.data = NULL; - handler->buffer.used = 0; - handler->buffer.size = 0; - } + handler->buffer.data = NULL; + handler->buffer.used = 0; + handler->buffer.size = 0; break; case PHP_OUTPUT_HANDLER_NO_DATA: /* handler ate all */ php_output_context_reset(context); ZEND_FALLTHROUGH; case PHP_OUTPUT_HANDLER_SUCCESS: - if (still_have_handler) { - /* no more buffered data */ - handler->buffer.used = 0; - handler->flags |= PHP_OUTPUT_HANDLER_PROCESSED; - } + /* no more buffered data */ + handler->buffer.used = 0; + handler->flags |= PHP_OUTPUT_HANDLER_PROCESSED; break; } diff --git a/tests/output/ob_start_callback_bad_return/handler_removed.phpt b/tests/output/ob_start_callback_bad_return/handler_false_removed.phpt similarity index 92% rename from tests/output/ob_start_callback_bad_return/handler_removed.phpt rename to tests/output/ob_start_callback_bad_return/handler_false_removed.phpt index c524ed04029f4..1614e7f6e4d7c 100644 --- a/tests/output/ob_start_callback_bad_return/handler_removed.phpt +++ b/tests/output/ob_start_callback_bad_return/handler_false_removed.phpt @@ -1,5 +1,5 @@ --TEST-- -ob_start(): Check behaviour with deprecation when OOM triggers handler removal +ob_start(): Check behaviour with deprecation when OOM triggers handler removal (handler returns false) --FILE-- +--EXPECTF-- +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d + +Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d diff --git a/tests/output/ob_start_callback_bad_return/handler_zero_removed.phpt b/tests/output/ob_start_callback_bad_return/handler_zero_removed.phpt new file mode 100644 index 0000000000000..f9bbd7d379276 --- /dev/null +++ b/tests/output/ob_start_callback_bad_return/handler_zero_removed.phpt @@ -0,0 +1,21 @@ +--TEST-- +ob_start(): Check behaviour with deprecation when OOM triggers handler removal (handler returns zero) +--FILE-- + +--EXPECTF-- +Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d + +Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d From 771024010243d1e909df8a01d70b4a29a23ba9ef Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Thu, 26 Jun 2025 13:37:59 -0700 Subject: [PATCH 08/15] No tabs --- .../exception_handler.phpt | 38 +++++++++---------- .../exception_handler_nested.phpt | 32 ++++++++-------- .../multiple_handlers.phpt | 30 +++++++-------- 3 files changed, 50 insertions(+), 50 deletions(-) diff --git a/tests/output/ob_start_callback_bad_return/exception_handler.phpt b/tests/output/ob_start_callback_bad_return/exception_handler.phpt index 413f855d40cc4..efe9ffc4516cb 100644 --- a/tests/output/ob_start_callback_bad_return/exception_handler.phpt +++ b/tests/output/ob_start_callback_bad_return/exception_handler.phpt @@ -10,42 +10,42 @@ set_error_handler(function (int $errno, string $errstr, string $errfile, int $er }); function return_null($string) { - global $log; - $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; return null; } function return_false($string) { - global $log; - $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; return false; } function return_true($string) { - global $log; - $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; return true; } function return_zero($string) { - global $log; - $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; return 0; } $cases = ['return_null', 'return_false', 'return_true', 'return_zero']; foreach ($cases as $case) { - $log = []; - echo "\n\nTesting: $case\n"; - ob_start($case); - echo "Inside of $case\n"; - try { - ob_end_flush(); - } catch (\ErrorException $e) { - echo $e . "\n"; - } - echo "\nEnd of $case, log was:\n"; - echo implode("\n", $log); + $log = []; + echo "\n\nTesting: $case\n"; + ob_start($case); + echo "Inside of $case\n"; + try { + ob_end_flush(); + } catch (\ErrorException $e) { + echo $e . "\n"; + } + echo "\nEnd of $case, log was:\n"; + echo implode("\n", $log); } ?> diff --git a/tests/output/ob_start_callback_bad_return/exception_handler_nested.phpt b/tests/output/ob_start_callback_bad_return/exception_handler_nested.phpt index 5983c1a626aee..29f17f99b649c 100644 --- a/tests/output/ob_start_callback_bad_return/exception_handler_nested.phpt +++ b/tests/output/ob_start_callback_bad_return/exception_handler_nested.phpt @@ -10,26 +10,26 @@ set_error_handler(function (int $errno, string $errstr, string $errfile, int $er }); function return_null($string) { - global $log; - $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; return null; } function return_false($string) { - global $log; - $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; return false; } function return_true($string) { - global $log; - $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; return true; } function return_zero($string) { - global $log; - $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; return 0; } @@ -40,30 +40,30 @@ ob_start('return_zero'); echo "In all of them\n\n"; try { - ob_end_flush(); + ob_end_flush(); } catch (\ErrorException $e) { - echo $e->getMessage() . "\n"; + echo $e->getMessage() . "\n"; } echo "Ended return_zero handler\n\n"; try { - ob_end_flush(); + ob_end_flush(); } catch (\ErrorException $e) { - echo $e->getMessage() . "\n"; + echo $e->getMessage() . "\n"; } echo "Ended return_true handler\n\n"; try { - ob_end_flush(); + ob_end_flush(); } catch (\ErrorException $e) { - echo $e->getMessage() . "\n"; + echo $e->getMessage() . "\n"; } echo "Ended return_false handler\n\n"; try { - ob_end_flush(); + ob_end_flush(); } catch (\ErrorException $e) { - echo $e->getMessage() . "\n"; + echo $e->getMessage() . "\n"; } echo "Ended return_null handler\n\n"; diff --git a/tests/output/ob_start_callback_bad_return/multiple_handlers.phpt b/tests/output/ob_start_callback_bad_return/multiple_handlers.phpt index 33ae03f9e5589..fef8daf8783a2 100644 --- a/tests/output/ob_start_callback_bad_return/multiple_handlers.phpt +++ b/tests/output/ob_start_callback_bad_return/multiple_handlers.phpt @@ -6,44 +6,44 @@ ob_start(): Check behaviour with multiple nested handlers with had return values $log = []; function return_given_string($string) { - global $log; - $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; - return $string; + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return $string; } function return_empty_string($string) { - global $log; - $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; return ""; } function return_false($string) { - global $log; - $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; return false; } function return_true($string) { - global $log; - $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; return true; } function return_null($string) { - global $log; - $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; return null; } function return_string($string) { - global $log; - $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; return "I stole your output."; } function return_zero($string) { - global $log; - $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; return 0; } From 541028c211653fd57682e354581d02cf5a980db1 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Fri, 27 Jun 2025 17:55:06 -0700 Subject: [PATCH 09/15] Memory limits --- .../ob_start_callback_bad_return/handler_false_removed.phpt | 2 ++ .../ob_start_callback_bad_return/handler_true_removed.phpt | 2 ++ .../ob_start_callback_bad_return/handler_zero_removed.phpt | 2 ++ 3 files changed, 6 insertions(+) diff --git a/tests/output/ob_start_callback_bad_return/handler_false_removed.phpt b/tests/output/ob_start_callback_bad_return/handler_false_removed.phpt index 1614e7f6e4d7c..873c3d2477634 100644 --- a/tests/output/ob_start_callback_bad_return/handler_false_removed.phpt +++ b/tests/output/ob_start_callback_bad_return/handler_false_removed.phpt @@ -1,5 +1,7 @@ --TEST-- ob_start(): Check behaviour with deprecation when OOM triggers handler removal (handler returns false) +--INI-- +memory_limit=2M --FILE-- Date: Thu, 3 Jul 2025 11:59:08 -0700 Subject: [PATCH 10/15] Update tests for rebase --- .../ob_start_callback_bad_return/handler_false_removed.phpt | 2 +- .../ob_start_callback_bad_return/handler_true_removed.phpt | 2 +- .../ob_start_callback_bad_return/handler_zero_removed.phpt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/output/ob_start_callback_bad_return/handler_false_removed.phpt b/tests/output/ob_start_callback_bad_return/handler_false_removed.phpt index 873c3d2477634..32702a58fcc14 100644 --- a/tests/output/ob_start_callback_bad_return/handler_false_removed.phpt +++ b/tests/output/ob_start_callback_bad_return/handler_false_removed.phpt @@ -18,6 +18,6 @@ while (1) { ?> --EXPECTF-- -Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d +Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d diff --git a/tests/output/ob_start_callback_bad_return/handler_true_removed.phpt b/tests/output/ob_start_callback_bad_return/handler_true_removed.phpt index 6e08e434ce873..5ad19826c4ac7 100644 --- a/tests/output/ob_start_callback_bad_return/handler_true_removed.phpt +++ b/tests/output/ob_start_callback_bad_return/handler_true_removed.phpt @@ -18,6 +18,6 @@ while (1) { ?> --EXPECTF-- -Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d +Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d diff --git a/tests/output/ob_start_callback_bad_return/handler_zero_removed.phpt b/tests/output/ob_start_callback_bad_return/handler_zero_removed.phpt index 91b1c69710a42..1bc7279c71d35 100644 --- a/tests/output/ob_start_callback_bad_return/handler_zero_removed.phpt +++ b/tests/output/ob_start_callback_bad_return/handler_zero_removed.phpt @@ -18,6 +18,6 @@ while (1) { ?> --EXPECTF-- -Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d +Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d From 47c9836666af377904d9d777415ff5b819e5502a Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Thu, 3 Jul 2025 12:00:05 -0700 Subject: [PATCH 11/15] Better variable name --- main/output.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/output.c b/main/output.c index 1437eb37273ba..a65be4547825a 100644 --- a/main/output.c +++ b/main/output.c @@ -979,8 +979,8 @@ static inline php_output_handler_status_t php_output_handler_op(php_output_handl int handler_count = php_output_get_level(); if (handler_count) { php_output_handler **handlers = (php_output_handler **) zend_stack_base(&OG(handlers)); - for (int iii = 0; iii < handler_count; ++iii) { - php_output_handler *curr_handler = handlers[iii]; + for (int handler_num = 0; handler_num < handler_count; ++handler_num) { + php_output_handler *curr_handler = handlers[handler_num]; if (curr_handler == handler) { handler->flags &= (~PHP_OUTPUT_HANDLER_DISABLED); still_have_handler = true; From 198a391d41c6622ad95d8e7e38bc8aff604c3786 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Thu, 3 Jul 2025 12:07:11 -0700 Subject: [PATCH 12/15] Test returning objects --- .../exception_handler.phpt | 53 ++++++++++++++++++- .../exception_handler_nested.phpt | 48 ++++++++++++++++- .../handler_is_stringable_removed.phpt | 30 +++++++++++ .../handler_non_stringable_removed.phpt | 32 +++++++++++ 4 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 tests/output/ob_start_callback_bad_return/handler_is_stringable_removed.phpt create mode 100644 tests/output/ob_start_callback_bad_return/handler_non_stringable_removed.phpt diff --git a/tests/output/ob_start_callback_bad_return/exception_handler.phpt b/tests/output/ob_start_callback_bad_return/exception_handler.phpt index efe9ffc4516cb..890f0855c5bf1 100644 --- a/tests/output/ob_start_callback_bad_return/exception_handler.phpt +++ b/tests/output/ob_start_callback_bad_return/exception_handler.phpt @@ -3,6 +3,16 @@ ob_start(): Check behaviour with deprecation converted to exception --FILE-- val; + } +} + $log = []; set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) { @@ -33,7 +43,26 @@ function return_zero($string) { return 0; } -$cases = ['return_null', 'return_false', 'return_true', 'return_zero']; +function return_non_stringable($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return new NotStringable($string); +} + +function return_stringable($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return new IsStringable($string); +} + +$cases = [ + 'return_null', + 'return_false', + 'return_true', + 'return_zero', + 'return_non_stringable', + 'return_stringable', +]; foreach ($cases as $case) { $log = []; echo "\n\nTesting: $case\n"; @@ -94,3 +123,25 @@ Stack trace: End of return_zero, log was: return_zero: <<>> + +Testing: return_non_stringable +ErrorException: ob_end_flush(): Returning a non-string result from user output handler return_non_stringable is deprecated in %s:%d +Stack trace: +#0 [internal function]: {closure:%s:%d}(8192, 'ob_end_flush():...', '/usr/src/php/te...', 69) +#1 %s(%d): ob_end_flush() +#2 {main} + +End of return_non_stringable, log was: +return_non_stringable: <<>> + +Testing: return_stringable +ErrorException: ob_end_flush(): Returning a non-string result from user output handler return_stringable is deprecated in %s:%d +Stack trace: +#0 [internal function]: {closure:%s:%d}(8192, 'ob_end_flush():...', '/usr/src/php/te...', 69) +#1 %s(%d): ob_end_flush() +#2 {main} + +End of return_stringable, log was: +return_stringable: <<>> diff --git a/tests/output/ob_start_callback_bad_return/exception_handler_nested.phpt b/tests/output/ob_start_callback_bad_return/exception_handler_nested.phpt index 29f17f99b649c..fcc58ba1cc5f4 100644 --- a/tests/output/ob_start_callback_bad_return/exception_handler_nested.phpt +++ b/tests/output/ob_start_callback_bad_return/exception_handler_nested.phpt @@ -3,6 +3,16 @@ ob_start(): Check behaviour with deprecation converted to exception --FILE-- val; + } +} + $log = []; set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) { @@ -33,12 +43,40 @@ function return_zero($string) { return 0; } +function return_non_stringable($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return new NotStringable($string); +} + +function return_stringable($string) { + global $log; + $log[] = __FUNCTION__ . ": <<<" . $string . ">>>"; + return new IsStringable($string); +} + ob_start('return_null'); ob_start('return_false'); ob_start('return_true'); ob_start('return_zero'); +ob_start('return_non_stringable'); +ob_start('return_stringable'); echo "In all of them\n\n"; +try { + ob_end_flush(); +} catch (\ErrorException $e) { + echo $e->getMessage() . "\n"; +} +echo "Ended return_stringable handler\n\n"; + +try { + ob_end_flush(); +} catch (\ErrorException $e) { + echo $e->getMessage() . "\n"; +} +echo "Ended return_non_stringable handler\n\n"; + try { ob_end_flush(); } catch (\ErrorException $e) { @@ -77,7 +115,15 @@ Ended return_null handler All handlers are over -return_zero: <<>> +return_non_stringable: <<>> +return_zero: <<>> return_true: <<<0ob_end_flush(): Returning a non-string result from user output handler return_zero is deprecated diff --git a/tests/output/ob_start_callback_bad_return/handler_is_stringable_removed.phpt b/tests/output/ob_start_callback_bad_return/handler_is_stringable_removed.phpt new file mode 100644 index 0000000000000..0d87358da1b9d --- /dev/null +++ b/tests/output/ob_start_callback_bad_return/handler_is_stringable_removed.phpt @@ -0,0 +1,30 @@ +--TEST-- +ob_start(): Check behaviour with deprecation when OOM triggers handler removal (handler returns stringable object) +--INI-- +memory_limit=2M +--FILE-- +val; + } +} + +ob_start(function() { + // We are out of memory, now trigger a deprecation + return new IsStringable(""); +}); + +$a = []; +// trigger OOM in a resize operation +while (1) { + $a[] = 1; +} + +?> +--EXPECTF-- +Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d + +Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d diff --git a/tests/output/ob_start_callback_bad_return/handler_non_stringable_removed.phpt b/tests/output/ob_start_callback_bad_return/handler_non_stringable_removed.phpt new file mode 100644 index 0000000000000..65d8ccfbcba61 --- /dev/null +++ b/tests/output/ob_start_callback_bad_return/handler_non_stringable_removed.phpt @@ -0,0 +1,32 @@ +--TEST-- +ob_start(): Check behaviour with deprecation when OOM triggers handler removal (handler returns non-stringable object) +--INI-- +memory_limit=2M +--FILE-- + +--EXPECTF-- +Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d + +Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d + +Fatal error: Uncaught Error: Object of class NotStringable could not be converted to string in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d From 40aa1050554fecc9e730f22ddbb8cf4723d35066 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Thu, 3 Jul 2025 12:26:47 -0700 Subject: [PATCH 13/15] More test fixes --- Zend/tests/concat/bug79836.phpt | 6 +++--- Zend/tests/concat/bug79836_1.phpt | 4 ++-- Zend/tests/concat/bug79836_2.phpt | 4 ++-- Zend/tests/declare/gh18033_2.phpt | 2 +- Zend/tests/gh11189.phpt | 2 +- Zend/tests/gh11189_1.phpt | 2 +- Zend/tests/gh16408.phpt | 6 +++--- sapi/cli/tests/gh8827-002.phpt | 10 +++++----- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Zend/tests/concat/bug79836.phpt b/Zend/tests/concat/bug79836.phpt index 0383bd0fb6368..68f73875c5775 100644 --- a/Zend/tests/concat/bug79836.phpt +++ b/Zend/tests/concat/bug79836.phpt @@ -15,9 +15,9 @@ ob_end_clean(); echo $counter . "\n"; ?> --EXPECTF-- -Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d +Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d -Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d +Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d -Deprecated: ob_end_clean(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d +Deprecated: ob_end_clean(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d 3 diff --git a/Zend/tests/concat/bug79836_1.phpt b/Zend/tests/concat/bug79836_1.phpt index 28a04ad47f5cc..ba9db33130b10 100644 --- a/Zend/tests/concat/bug79836_1.phpt +++ b/Zend/tests/concat/bug79836_1.phpt @@ -15,7 +15,7 @@ ob_end_clean(); echo "Done\n"; ?> --EXPECTF-- -Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d +Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d -Deprecated: ob_end_clean(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d +Deprecated: ob_end_clean(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d Done diff --git a/Zend/tests/concat/bug79836_2.phpt b/Zend/tests/concat/bug79836_2.phpt index eeba0135468cd..c03b6ce10cd5b 100644 --- a/Zend/tests/concat/bug79836_2.phpt +++ b/Zend/tests/concat/bug79836_2.phpt @@ -22,7 +22,7 @@ ob_end_clean(); echo $x . "\n"; ?> --EXPECTF-- -Deprecated: X::__toString(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d +Deprecated: X::__toString(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d -Deprecated: ob_end_clean(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d +Deprecated: ob_end_clean(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabc diff --git a/Zend/tests/declare/gh18033_2.phpt b/Zend/tests/declare/gh18033_2.phpt index 45d506b9b1d87..ade70d18f29f0 100644 --- a/Zend/tests/declare/gh18033_2.phpt +++ b/Zend/tests/declare/gh18033_2.phpt @@ -14,4 +14,4 @@ ob_start(function() { }); ?> --EXPECTF-- -Deprecated: PHP Request Shutdown: Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d +Deprecated: PHP Request Shutdown: Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d diff --git a/Zend/tests/gh11189.phpt b/Zend/tests/gh11189.phpt index e7a2ee66625df..3b8130905a58a 100644 --- a/Zend/tests/gh11189.phpt +++ b/Zend/tests/gh11189.phpt @@ -29,6 +29,6 @@ while (1) { ?> --EXPECTF-- Success -Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d +Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d Fatal error: Allowed memory size of %s bytes exhausted%s(tried to allocate %s bytes) in %s on line %d diff --git a/Zend/tests/gh11189_1.phpt b/Zend/tests/gh11189_1.phpt index a0891c1c99c55..3f7a0bfe389f3 100644 --- a/Zend/tests/gh11189_1.phpt +++ b/Zend/tests/gh11189_1.phpt @@ -29,6 +29,6 @@ while (1) { ?> --EXPECTF-- Success -Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d +Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d Fatal error: Allowed memory size of %s bytes exhausted%s(tried to allocate %s bytes) in %s on line %d diff --git a/Zend/tests/gh16408.phpt b/Zend/tests/gh16408.phpt index de09c84803dea..f579015102067 100644 --- a/Zend/tests/gh16408.phpt +++ b/Zend/tests/gh16408.phpt @@ -13,9 +13,9 @@ ob_end_clean(); echo $counter . "\n"; ?> --EXPECTF-- -Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d +Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d -Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d +Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d -Deprecated: ob_end_clean(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d +Deprecated: ob_end_clean(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d 3 diff --git a/sapi/cli/tests/gh8827-002.phpt b/sapi/cli/tests/gh8827-002.phpt index 72e2aeb077c3f..3ee05fcc724b6 100644 --- a/sapi/cli/tests/gh8827-002.phpt +++ b/sapi/cli/tests/gh8827-002.phpt @@ -35,17 +35,17 @@ var_dump(@fopen('php://stdout', 'a')); --EXPECTF-- STDIN: -Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d +Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d bool(false) -Deprecated: var_dump(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d +Deprecated: var_dump(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d STDERR: -Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d +Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d bool(false) -Deprecated: var_dump(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d +Deprecated: var_dump(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d STDOUT: -Deprecated: main(): Returning a non-string result from user output handler Closure::__invoke is deprecated in %s on line %d +Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d bool(false) From 1cb076fd70f3c5606c9995b3e70803ea2332be9b Mon Sep 17 00:00:00 2001 From: DanielEScherzer Date: Thu, 3 Jul 2025 13:08:44 -0700 Subject: [PATCH 14/15] Update exception_handler.phpt --- .../ob_start_callback_bad_return/exception_handler.phpt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/output/ob_start_callback_bad_return/exception_handler.phpt b/tests/output/ob_start_callback_bad_return/exception_handler.phpt index 890f0855c5bf1..3a3ace427bbe0 100644 --- a/tests/output/ob_start_callback_bad_return/exception_handler.phpt +++ b/tests/output/ob_start_callback_bad_return/exception_handler.phpt @@ -127,7 +127,7 @@ return_zero: << Date: Sun, 6 Jul 2025 10:52:51 -0700 Subject: [PATCH 15/15] Variable name + tests --- Zend/tests/concat/bug79836.phpt | 8 ++------ Zend/tests/concat/bug79836_1.phpt | 6 ++---- Zend/tests/concat/bug79836_2.phpt | 6 ++---- Zend/tests/declare/gh18033_2.phpt | 4 ++-- Zend/tests/gh11189.phpt | 3 +-- Zend/tests/gh11189_1.phpt | 3 +-- Zend/tests/gh16408.phpt | 8 ++------ ext/session/tests/user_session_module/bug61728.phpt | 5 ++--- main/output.c | 4 ++-- sapi/cli/tests/gh8827-002.phpt | 13 ++----------- 10 files changed, 18 insertions(+), 42 deletions(-) diff --git a/Zend/tests/concat/bug79836.phpt b/Zend/tests/concat/bug79836.phpt index 68f73875c5775..87db276337098 100644 --- a/Zend/tests/concat/bug79836.phpt +++ b/Zend/tests/concat/bug79836.phpt @@ -8,16 +8,12 @@ $counter = 0; ob_start(function ($buffer) use (&$c, &$counter) { $c = 0; ++$counter; + return ''; }, 1); $c .= []; $c .= []; ob_end_clean(); echo $counter . "\n"; ?> ---EXPECTF-- -Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d - -Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d - -Deprecated: ob_end_clean(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d +--EXPECT-- 3 diff --git a/Zend/tests/concat/bug79836_1.phpt b/Zend/tests/concat/bug79836_1.phpt index ba9db33130b10..972b387235698 100644 --- a/Zend/tests/concat/bug79836_1.phpt +++ b/Zend/tests/concat/bug79836_1.phpt @@ -7,6 +7,7 @@ opcache.optimization_level = 0x7FFEBFFF & ~0x400 $x = 'non-empty'; ob_start(function () use (&$c) { $c = 0; + return ''; }, 1); $c = []; $x = $c . $x; @@ -14,8 +15,5 @@ $x = $c . $x; ob_end_clean(); echo "Done\n"; ?> ---EXPECTF-- -Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d - -Deprecated: ob_end_clean(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d +--EXPECT-- Done diff --git a/Zend/tests/concat/bug79836_2.phpt b/Zend/tests/concat/bug79836_2.phpt index c03b6ce10cd5b..de528e5c42fac 100644 --- a/Zend/tests/concat/bug79836_2.phpt +++ b/Zend/tests/concat/bug79836_2.phpt @@ -6,6 +6,7 @@ $c = str_repeat("abcd", 10); ob_start(function () use (&$c) { $c = 0; + return ''; }, 1); class X { @@ -21,8 +22,5 @@ $x = $c . $xxx; ob_end_clean(); echo $x . "\n"; ?> ---EXPECTF-- -Deprecated: X::__toString(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d - -Deprecated: ob_end_clean(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d +--EXPECT-- abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabc diff --git a/Zend/tests/declare/gh18033_2.phpt b/Zend/tests/declare/gh18033_2.phpt index ade70d18f29f0..39fb46144eed5 100644 --- a/Zend/tests/declare/gh18033_2.phpt +++ b/Zend/tests/declare/gh18033_2.phpt @@ -11,7 +11,7 @@ ob_start(function() { register_tick_function( function() { } ); + return ''; }); ?> ---EXPECTF-- -Deprecated: PHP Request Shutdown: Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d +--EXPECT-- diff --git a/Zend/tests/gh11189.phpt b/Zend/tests/gh11189.phpt index 3b8130905a58a..9f8202b6102b3 100644 --- a/Zend/tests/gh11189.phpt +++ b/Zend/tests/gh11189.phpt @@ -18,6 +18,7 @@ ob_start(function() { $a[] = 2; } fwrite(STDOUT, "Success"); + return ''; }); $a = []; @@ -29,6 +30,4 @@ while (1) { ?> --EXPECTF-- Success -Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d - Fatal error: Allowed memory size of %s bytes exhausted%s(tried to allocate %s bytes) in %s on line %d diff --git a/Zend/tests/gh11189_1.phpt b/Zend/tests/gh11189_1.phpt index 3f7a0bfe389f3..d5b4ee92b62d6 100644 --- a/Zend/tests/gh11189_1.phpt +++ b/Zend/tests/gh11189_1.phpt @@ -18,6 +18,7 @@ ob_start(function() { $a[] = 2; } fwrite(STDOUT, "Success"); + return ''; }); $a = ["not packed" => 1]; @@ -29,6 +30,4 @@ while (1) { ?> --EXPECTF-- Success -Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d - Fatal error: Allowed memory size of %s bytes exhausted%s(tried to allocate %s bytes) in %s on line %d diff --git a/Zend/tests/gh16408.phpt b/Zend/tests/gh16408.phpt index f579015102067..09d9e8cbb5249 100644 --- a/Zend/tests/gh16408.phpt +++ b/Zend/tests/gh16408.phpt @@ -6,16 +6,12 @@ $counter = 0; ob_start(function ($buffer) use (&$c, &$counter) { $c = 0; ++$counter; + return ''; }, 1); $c .= []; $c .= []; ob_end_clean(); echo $counter . "\n"; ?> ---EXPECTF-- -Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d - -Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d - -Deprecated: ob_end_clean(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d +--EXPECT-- 3 diff --git a/ext/session/tests/user_session_module/bug61728.phpt b/ext/session/tests/user_session_module/bug61728.phpt index 7bc042e0116fa..152cf9f42beef 100644 --- a/ext/session/tests/user_session_module/bug61728.phpt +++ b/ext/session/tests/user_session_module/bug61728.phpt @@ -5,7 +5,7 @@ session --FILE-- ---EXPECTF-- -Deprecated: ob_end_flush(): Returning a non-string result from user output handler output_html is deprecated in %s on line %d +--EXPECT-- 8 diff --git a/main/output.c b/main/output.c index a65be4547825a..7232044ebeb0f 100644 --- a/main/output.c +++ b/main/output.c @@ -1270,8 +1270,8 @@ static int php_output_stack_pop(int flags) int handler_count = php_output_get_level(); if (handler_count) { php_output_handler **handlers = (php_output_handler **) zend_stack_base(&OG(handlers)); - for (int iii = 0; iii < handler_count; ++iii) { - php_output_handler *curr_handler = handlers[iii]; + for (int handler_num = 0; handler_num < handler_count; ++handler_num) { + php_output_handler *curr_handler = handlers[handler_num]; if (curr_handler == orphan) { still_have_handler = true; break; diff --git a/sapi/cli/tests/gh8827-002.phpt b/sapi/cli/tests/gh8827-002.phpt index 3ee05fcc724b6..712d3e54e509f 100644 --- a/sapi/cli/tests/gh8827-002.phpt +++ b/sapi/cli/tests/gh8827-002.phpt @@ -18,6 +18,7 @@ $stderr = fopen('php://stderr', 'r'); ob_start(function ($buffer) use ($stdout) { fwrite($stdout, $buffer); + return ''; }, 1); print "STDIN:\n"; @@ -32,20 +33,10 @@ print "STDOUT:\n"; fclose(STDOUT); var_dump(@fopen('php://stdout', 'a')); ?> ---EXPECTF-- +--EXPECT-- STDIN: - -Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d bool(false) - -Deprecated: var_dump(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d STDERR: - -Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d bool(false) - -Deprecated: var_dump(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d STDOUT: - -Deprecated: main(): Returning a non-string result from user output handler {closure:%s:%d} is deprecated in %s on line %d bool(false)