From c6fd164e4fb914ce16676f1b0cfc8db945059c7a Mon Sep 17 00:00:00 2001 From: lazerg Date: Sun, 11 Oct 2026 15:35:41 +0500 Subject: [PATCH 1/3] Fix GH-24256: fclose() returns false for wrappers without stream_flush() --- NEWS | 2 ++ ext/standard/tests/file/gh24256.phpt | 28 ++++++++++++++++++++++++++++ main/streams/userspace.c | 2 +- 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 ext/standard/tests/file/gh24256.phpt diff --git a/NEWS b/NEWS index 7abff44fbb10..8308b63c1141 100644 --- a/NEWS +++ b/NEWS @@ -40,6 +40,8 @@ PHP NEWS when getpwnam_r() needs a larger buffer. (Ilia Alshanetsky) . Fixed password_get_info() and password_needs_rehash() accepting malformed bcrypt costs. (Ilia Alshanetsky) + . Fixed bug GH-24256 (fclose() returns false with stream wrappers that do + not implement stream_flush()). (Lazizbek Ergashev) - Zip: . Fixed use-after-free when re-entering ZipArchive during destruction or diff --git a/ext/standard/tests/file/gh24256.phpt b/ext/standard/tests/file/gh24256.phpt new file mode 100644 index 000000000000..ebdba1c443d9 --- /dev/null +++ b/ext/standard/tests/file/gh24256.phpt @@ -0,0 +1,28 @@ +--TEST-- +GH-24256 (fclose() returns false for stream wrappers without stream_flush()) +--FILE-- + +--EXPECT-- +int(4) +bool(true) +int(4) diff --git a/main/streams/userspace.c b/main/streams/userspace.c index b42d7f00e973..b7ad9c9a71eb 100644 --- a/main/streams/userspace.c +++ b/main/streams/userspace.c @@ -741,7 +741,7 @@ static int php_userstreamop_flush(php_stream *stream) zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 0, NULL); zend_string_release_ex(func_name, false); - int ret = call_result == SUCCESS && Z_TYPE(retval) != IS_UNDEF && zend_is_true(&retval) ? 0 : -1; + int ret = call_result == FAILURE || (Z_TYPE(retval) != IS_UNDEF && zend_is_true(&retval)) ? 0 : -1; zval_ptr_dtor(&retval); From d76db20124ccb53443e6333ff1530d2a1e3c2d53 Mon Sep 17 00:00:00 2001 From: lazerg Date: Sun, 11 Oct 2026 16:07:50 +0500 Subject: [PATCH 2/3] Only ignore a missing stream_flush() when the stream is closed --- main/streams/userspace.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/main/streams/userspace.c b/main/streams/userspace.c index b7ad9c9a71eb..28c033da80fb 100644 --- a/main/streams/userspace.c +++ b/main/streams/userspace.c @@ -741,7 +741,12 @@ static int php_userstreamop_flush(php_stream *stream) zend_result call_result = zend_call_method_if_exists(Z_OBJ(us->object), func_name, &retval, 0, NULL); zend_string_release_ex(func_name, false); - int ret = call_result == FAILURE || (Z_TYPE(retval) != IS_UNDEF && zend_is_true(&retval)) ? 0 : -1; + int ret; + if (call_result == FAILURE) { + ret = stream->in_free ? 0 : -1; + } else { + ret = Z_TYPE(retval) != IS_UNDEF && zend_is_true(&retval) ? 0 : -1; + } zval_ptr_dtor(&retval); From 0c82c6a504112c43c73c55be6fc6b12c23bc58d9 Mon Sep 17 00:00:00 2001 From: lazerg Date: Sun, 11 Oct 2026 16:28:27 +0500 Subject: [PATCH 3/3] Add comment, cover copy() in test, align NEWS wording --- NEWS | 4 ++-- ext/standard/tests/file/gh24256.phpt | 6 ++++++ main/streams/userspace.c | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/NEWS b/NEWS index 8308b63c1141..6fe3607e3f73 100644 --- a/NEWS +++ b/NEWS @@ -40,8 +40,8 @@ PHP NEWS when getpwnam_r() needs a larger buffer. (Ilia Alshanetsky) . Fixed password_get_info() and password_needs_rehash() accepting malformed bcrypt costs. (Ilia Alshanetsky) - . Fixed bug GH-24256 (fclose() returns false with stream wrappers that do - not implement stream_flush()). (Lazizbek Ergashev) + . Fixed bug GH-24256 (fclose() returns false for stream wrappers without + stream_flush()). (Lazizbek Ergashev) - Zip: . Fixed use-after-free when re-entering ZipArchive during destruction or diff --git a/ext/standard/tests/file/gh24256.phpt b/ext/standard/tests/file/gh24256.phpt index ebdba1c443d9..c1b217349e0e 100644 --- a/ext/standard/tests/file/gh24256.phpt +++ b/ext/standard/tests/file/gh24256.phpt @@ -12,6 +12,10 @@ class TestStream { function stream_write($data) { return strlen($data); } + + function url_stat($path, $flags) { + return false; + } } stream_wrapper_register('test', TestStream::class); @@ -21,8 +25,10 @@ var_dump(fwrite($fp, 'data')); var_dump(fclose($fp)); var_dump(file_put_contents('test://foo', 'data')); +var_dump(copy(__FILE__, 'test://foo')); ?> --EXPECT-- int(4) bool(true) int(4) +bool(true) diff --git a/main/streams/userspace.c b/main/streams/userspace.c index 28c033da80fb..d9d4aec1bb7a 100644 --- a/main/streams/userspace.c +++ b/main/streams/userspace.c @@ -743,6 +743,7 @@ static int php_userstreamop_flush(php_stream *stream) int ret; if (call_result == FAILURE) { + /* stream_flush is optional, so a missing one is not an error when closing */ ret = stream->in_free ? 0 : -1; } else { ret = Z_TYPE(retval) != IS_UNDEF && zend_is_true(&retval) ? 0 : -1;