From 28df010160c81db25eb96a4089beeb825c1c9d24 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Wed, 11 Nov 2026 11:14:57 -0800 Subject: [PATCH] ext/standard: fix use-after-free and resource reuse from `write_seek_mode` When converting an object into a string for `write_seek_mode`, don't allow the target stream to be closed, otherwise it could trigger a use-after-free or silently reuse the stream. --- ext/standard/streamsfuncs.c | 5 +++ .../streams/write_seek_mode-close-uaf.phpt | 33 ++++++++++++++ .../tests/streams/write_seek_mode-reuse.phpt | 43 +++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 ext/standard/tests/streams/write_seek_mode-close-uaf.phpt create mode 100644 ext/standard/tests/streams/write_seek_mode-reuse.phpt diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index 6de7d255e067..e132c8b441f4 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -1339,7 +1339,12 @@ static void apply_filter_to_stream(bool append, INTERNAL_FUNCTION_PARAMETERS) } if (read_write & PHP_STREAM_FILTER_WRITE) { + bool originally_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE; + stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE; filter = php_stream_filter_create(filtername, filterparams, php_stream_is_persistent(stream)); + if (!originally_no_fclose) { + stream->flags ^= PHP_STREAM_FLAG_NO_FCLOSE; + } if (filter == NULL) { RETURN_FALSE; } diff --git a/ext/standard/tests/streams/write_seek_mode-close-uaf.phpt b/ext/standard/tests/streams/write_seek_mode-close-uaf.phpt new file mode 100644 index 000000000000..7000eaad2c97 --- /dev/null +++ b/ext/standard/tests/streams/write_seek_mode-close-uaf.phpt @@ -0,0 +1,33 @@ +--TEST-- +`write_seek_mode` with a stringable object closing the stream triggers UAF +--EXTENSIONS-- +zlib +--CREDITS-- +Calif.io +--FILE-- + new CloseStreamForWriteSeekMode()], +); + +var_dump(is_resource($filter), is_resource($stream)); +?> +--EXPECTF-- +Warning: fclose(): cannot close the provided stream, as it must not be manually closed in %s on line %d +bool(true) +bool(true) diff --git a/ext/standard/tests/streams/write_seek_mode-reuse.phpt b/ext/standard/tests/streams/write_seek_mode-reuse.phpt new file mode 100644 index 000000000000..72bb4cdb46f6 --- /dev/null +++ b/ext/standard/tests/streams/write_seek_mode-reuse.phpt @@ -0,0 +1,43 @@ +--TEST-- +`write_seek_mode` with a stringable object closing and reopening the stream triggers reuse +--EXTENSIONS-- +zlib +--CREDITS-- +Calif.io +--FILE-- + new ReplaceStreamForWriteSeekMode()], +); + +var_dump(is_resource($filter), is_resource($replacementStream)); +fwrite($replacementStream, str_repeat('A', 64)); +var_dump(stream_filter_remove($filter)); +rewind($replacementStream); +$raw = stream_get_contents($replacementStream); +var_dump(strlen($raw), bin2hex($raw)); +?> +--EXPECTF-- +Warning: fclose(): cannot close the provided stream, as it must not be manually closed in %s on line %d +bool(true) +bool(true) +bool(true) +int(64) +string(128) "41414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141"