diff --git a/NEWS b/NEWS index 3144cdf88c59..83cdbf662489 100644 --- a/NEWS +++ b/NEWS @@ -195,6 +195,8 @@ PHP NEWS . Allowed filtered streams to be casted as fd for select. (Jakub Zelenka) . Fixed bug GH-21221 (Prevent closing of innerstream of php://temp stream). (ilutov) + . Fixed bug GH-21700 (Assertion failure in _php_stream_seek when a user + stream returns a negative position). (lacatoire) . Improved stream_socket_server() bind failure error reporting. (ilutov) . Fixed bug #49874 (ftell() and fseek() inconsistency when using stream filters). (Jakub Zelenka) diff --git a/ext/standard/tests/streams/gh21700_user_stream_append_negative_position.phpt b/ext/standard/tests/streams/gh21700_user_stream_append_negative_position.phpt new file mode 100644 index 000000000000..f3037e3bd389 --- /dev/null +++ b/ext/standard/tests/streams/gh21700_user_stream_append_negative_position.phpt @@ -0,0 +1,27 @@ +--TEST-- +GH-21700: Append-mode open ignores a negative position returned by a user stream +--FILE-- + +--EXPECT-- +int(0) +int(-1) diff --git a/ext/standard/tests/streams/gh21700_user_stream_negative_position.phpt b/ext/standard/tests/streams/gh21700_user_stream_negative_position.phpt new file mode 100644 index 000000000000..fe9ea64a86a2 --- /dev/null +++ b/ext/standard/tests/streams/gh21700_user_stream_negative_position.phpt @@ -0,0 +1,48 @@ +--TEST-- +GH-21700: User stream returning negative position must not trigger an assertion +--FILE-- +pos = $offset; + return true; + } + public function stream_tell() { return $this->pos; } + public function stream_eof() { return false; } + public function stream_read($count) { return ''; } +} + +class lyingstream extends mystream { + public function stream_tell(): int { return -42; } +} + +// 1. SEEK_CUR with a negative resulting absolute position is rejected +// before invoking the user seek hook, so stream->position stays valid. +stream_wrapper_register("test", "mystream"); +$fp = fopen("test://foo", "rb"); +var_dump(fseek($fp, -1, SEEK_CUR)); +var_dump(ftell($fp)); +var_dump(fseek($fp, 0, SEEK_CUR)); +fclose($fp); +stream_wrapper_unregister("test"); + +// 2. A user stream_tell() returning a negative position is rejected, +// keeping stream->position non-negative. +stream_wrapper_register("test", "lyingstream"); +$fp = fopen("test://foo", "rb"); +var_dump(fseek($fp, 100, SEEK_SET)); +var_dump(ftell($fp)); +var_dump(fseek($fp, 0, SEEK_CUR)); +fclose($fp); +stream_wrapper_unregister("test"); +?> +--EXPECT-- +int(-1) +int(0) +int(0) +int(-1) +int(0) +int(-1) diff --git a/main/streams/streams.c b/main/streams/streams.c index 31d1eda16790..e097064ecb75 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -1466,19 +1466,31 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence) } else { offset = stream->position + offset; } - whence = SEEK_SET; - break; + whence = SEEK_SET; + ZEND_FALLTHROUGH; case SEEK_SET: if (offset < 0) { return -1; } + break; } + zend_off_t saved_position = stream->position; ret = stream->ops->seek(stream, offset, whence, &stream->position); if (((stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0) || ret == 0) { if (ret == 0) { - stream->eof = 0; - stream->fatal_error = 0; + if (UNEXPECTED(stream->position < 0)) { + /* The seek operation reported success but updated + * stream->position to a negative value (e.g. a user + * stream's stream_tell() returned a negative offset). + * Treat as failure and restore the original position + * to keep the invariant stream->position >= 0. */ + stream->position = saved_position; + ret = -1; + } else { + stream->eof = 0; + stream->fatal_error = 0; + } } /* invalidate the buffer contents */ @@ -2392,7 +2404,7 @@ PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mod zend_off_t newpos = 0; /* if opened for append, we need to revise our idea of the initial file position */ - if (0 == stream->ops->seek(stream, 0, SEEK_CUR, &newpos)) { + if (0 == stream->ops->seek(stream, 0, SEEK_CUR, &newpos) && newpos >= 0) { stream->position = newpos; } }