diff --git a/NEWS b/NEWS index 500f75cc050d..d7c0e9a1a2bf 100644 --- a/NEWS +++ b/NEWS @@ -34,6 +34,10 @@ PHP NEWS . Fixed read buffer compaction in php_stream_filter_flush(). (crystarm) . Io\Poll\Context::wait() now rejects a $maxEvents value greater than INT_MAX instead of truncating it. (marc-mabe) + . Fixed fseek() accepting $whence values that do not fit in an int, which + were silently truncated onto a valid seek constant. (lacatoire) + . Fixed a failed seek discarding the read buffer, which desynchronized the + stream from its reported position. (lacatoire) 27 Aug 2026, PHP 8.6.0beta2 diff --git a/ext/standard/file.c b/ext/standard/file.c index d6a8b9f1d0ea..7088d8c1cbd6 100644 --- a/ext/standard/file.c +++ b/ext/standard/file.c @@ -1104,6 +1104,10 @@ PHPAPI PHP_FUNCTION(fseek) Z_PARAM_LONG(whence) ZEND_PARSE_PARAMETERS_END(); + if (ZEND_LONG_EXCEEDS_INT(whence)) { + RETURN_LONG(-1); + } + php_stream_error_operation_begin(); RETVAL_LONG(php_stream_seek(stream, offset, (int) whence)); php_stream_error_operation_end_for_stream(stream); diff --git a/ext/standard/tests/file/fseek_whence_invalid_inrange.phpt b/ext/standard/tests/file/fseek_whence_invalid_inrange.phpt new file mode 100644 index 000000000000..c10fbcfbeb73 --- /dev/null +++ b/ext/standard/tests/file/fseek_whence_invalid_inrange.phpt @@ -0,0 +1,67 @@ +--TEST-- +fseek(): an invalid $whence that fits in an int must not desynchronize the stream +--FILE-- + $tmp, 'memory' => 'php://memory'] as $label => $target) { + echo $label, PHP_EOL; + $h = fopen($target, $label === 'file' ? 'r' : 'w+'); + if ($label === 'memory') { + fwrite($h, "0123456789"); + rewind($h); + } + var_dump(fread($h, 4)); + var_dump(fseek($h, 3, $whence)); + var_dump(ftell($h)); + var_dump(fread($h, 6)); + fclose($h); + } + + echo PHP_EOL; +} +?> +--CLEAN-- + +--EXPECT-- +whence=99 +file +string(4) "0123" +int(-1) +int(4) +string(6) "456789" +memory +string(4) "0123" +int(-1) +int(4) +string(6) "456789" + +whence=-2147483648 +file +string(4) "0123" +int(-1) +int(4) +string(6) "456789" +memory +string(4) "0123" +int(-1) +int(4) +string(6) "456789" + +whence=2147483647 +file +string(4) "0123" +int(-1) +int(4) +string(6) "456789" +memory +string(4) "0123" +int(-1) +int(4) +string(6) "456789" diff --git a/ext/standard/tests/file/fseek_whence_overflow.phpt b/ext/standard/tests/file/fseek_whence_overflow.phpt new file mode 100644 index 000000000000..46c5a0292ef1 --- /dev/null +++ b/ext/standard/tests/file/fseek_whence_overflow.phpt @@ -0,0 +1,49 @@ +--TEST-- +fseek(): $whence values that overflow int must return -1, not alias onto a valid constant +--SKIPIF-- + +--FILE-- + +--CLEAN-- + +--EXPECT-- +int(-1) +int(4) +int(-1) +int(4) +int(-1) +int(4) +int(0) +int(7) diff --git a/main/streams/streams.c b/main/streams/streams.c index a09a2180921d..bbc5047ab357 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -1367,6 +1367,7 @@ PHPAPI int php_stream_seek(php_stream *stream, zend_off_t offset, int whence) if (stream->ops->seek && (stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0) { + zend_off_t old_position = stream->position; int ret; switch(whence) { case SEEK_CUR: @@ -1386,6 +1387,13 @@ PHPAPI int php_stream_seek(php_stream *stream, zend_off_t offset, int whence) ret = stream->ops->seek(stream, offset, whence, &stream->position); if (((stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0) || ret == 0) { + if (ret != 0 && stream->position == old_position) { + /* the seek failed without moving the stream, so the buffered + * data and the filter state still describe the current + * position and must be left alone */ + return ret; + } + if (ret == 0) { stream->eof = 0; stream->fatal_error = 0;