From df3278d2b00ea2db4e60d3e26a28e78a485dd939 Mon Sep 17 00:00:00 2001 From: lacatoire Date: Tue, 18 Aug 2026 07:44:48 +0200 Subject: [PATCH 1/2] Fix fseek() accepting $whence values that do not fit in an int $whence is parsed as a zend_long but cast to a C int when handed to php_stream_seek(). Values whose low 32 bits alias onto a valid seek constant were accepted and acted upon: on a 64-bit build, SEEK_CUR + 2**32 seeks relative to the current position, SEEK_END + 2**32 relative to the end, and PHP_INT_MIN relative to the start, each returning 0 for success. Reject values outside the int range before the cast and return -1, which is already what fseek() returns for an invalid $whence that does fit in an int. Platform constants such as SEEK_DATA and SEEK_HOLE fit in an int and keep working. --- NEWS | 2 + ext/standard/file.c | 4 ++ .../tests/file/fseek_whence_overflow.phpt | 46 +++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 ext/standard/tests/file/fseek_whence_overflow.phpt diff --git a/NEWS b/NEWS index 500f75cc050d..ad55023bc255 100644 --- a/NEWS +++ b/NEWS @@ -34,6 +34,8 @@ 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) 27 Aug 2026, PHP 8.6.0beta2 diff --git a/ext/standard/file.c b/ext/standard/file.c index d6a8b9f1d0ea..c2b395b858de 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 (whence < INT_MIN || whence > INT_MAX) { + 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_overflow.phpt b/ext/standard/tests/file/fseek_whence_overflow.phpt new file mode 100644 index 000000000000..760659dd9ee0 --- /dev/null +++ b/ext/standard/tests/file/fseek_whence_overflow.phpt @@ -0,0 +1,46 @@ +--TEST-- +fseek(): $whence values that overflow int must return -1, not alias onto a valid constant +--SKIPIF-- + +--FILE-- + +--EXPECT-- +int(-1) +int(4) +int(-1) +int(4) +int(-1) +int(4) +int(0) +int(7) From 62d5985a6e34b94075295594c773efd2511acf8f Mon Sep 17 00:00:00 2001 From: Louis-Arnaud Catoire Date: Fri, 28 Aug 2026 14:44:49 +0200 Subject: [PATCH 2/2] Do not discard the read buffer when a seek fails php_stream_seek() invalidated the read buffer after calling ops->seek() even when that call reported a failure. On a plain file, an invalid $whence makes lseek() fail with EINVAL without moving the descriptor, so the buffered data was still valid for the reported position; dropping it desynchronized the stream: $h = fopen($f, 'r'); // 10-byte file fread($h, 4); // "0123" fseek($h, 3, 99); // -1, position left at 4 fread($h, 6); // "" instead of "456789" The buffer and the filter state are now left alone when ops->seek() fails without moving the stream. Implementations that do move on failure, such as php_stream_memory_seek() resetting fpos, still invalidate it. --- NEWS | 2 + .../file/fseek_whence_invalid_inrange.phpt | 40 +++++++++++++++++++ main/streams/streams.c | 8 ++++ 3 files changed, 50 insertions(+) create mode 100644 ext/standard/tests/file/fseek_whence_invalid_inrange.phpt diff --git a/NEWS b/NEWS index ad55023bc255..d7c0e9a1a2bf 100644 --- a/NEWS +++ b/NEWS @@ -36,6 +36,8 @@ PHP NEWS 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/tests/file/fseek_whence_invalid_inrange.phpt b/ext/standard/tests/file/fseek_whence_invalid_inrange.phpt new file mode 100644 index 000000000000..f0aba239b5f8 --- /dev/null +++ b/ext/standard/tests/file/fseek_whence_invalid_inrange.phpt @@ -0,0 +1,40 @@ +--TEST-- +fseek(): an invalid $whence that fits in an int must not desynchronize the stream +--FILE-- + +--CLEAN-- + +--EXPECT-- +whence=99 +string(4) "0123" +int(-1) +int(4) +string(6) "456789" + +whence=-2147483648 +string(4) "0123" +int(-1) +int(4) +string(6) "456789" + +whence=2147483647 +string(4) "0123" +int(-1) +int(4) +string(6) "456789" 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;