Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions ext/standard/file.c
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,10 @@ PHPAPI PHP_FUNCTION(fseek)
Z_PARAM_LONG(whence)
ZEND_PARSE_PARAMETERS_END();

if (whence < INT_MIN || whence > INT_MAX) {
Comment thread
lacatoire marked this conversation as resolved.
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);
Expand Down
40 changes: 40 additions & 0 deletions ext/standard/tests/file/fseek_whence_invalid_inrange.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
--TEST--
fseek(): an invalid $whence that fits in an int must not desynchronize the stream
--FILE--
<?php
$tmp = __DIR__ . '/fseek_whence_invalid_inrange.tmp';
file_put_contents($tmp, "0123456789");

foreach ([99, -2147483648, 2147483647] as $whence) {
echo 'whence=', $whence, PHP_EOL;
$h = fopen($tmp, 'r');
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--
<?php
@unlink(__DIR__ . '/fseek_whence_invalid_inrange.tmp');
?>
--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"
46 changes: 46 additions & 0 deletions ext/standard/tests/file/fseek_whence_overflow.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
--TEST--
fseek(): $whence values that overflow int must return -1, not alias onto a valid constant
--SKIPIF--
<?php
if (PHP_INT_SIZE < 8) die("skip 64-bit only");
?>
--FILE--
<?php
$tmp = tempnam(sys_get_temp_dir(), 'fseek');
file_put_contents($tmp, "0123456789");
$bias = 2 ** 32;

$h = fopen($tmp, 'r');

// SEEK_CUR + 2**32 must not alias onto SEEK_CUR (1)
fseek($h, 4);
var_dump(fseek($h, 3, SEEK_CUR + $bias)); // -1
var_dump(ftell($h)); // 4 (unchanged)

// SEEK_END + 2**32 must not alias onto SEEK_END (2)
fseek($h, 4);
var_dump(fseek($h, 3, SEEK_END + $bias)); // -1
var_dump(ftell($h)); // 4 (unchanged)

// PHP_INT_MIN must not alias onto SEEK_SET (0)
fseek($h, 4);
var_dump(fseek($h, 3, PHP_INT_MIN)); // -1
var_dump(ftell($h)); // 4 (unchanged)

// Sanity: normal SEEK_CUR still works
fseek($h, 4);
var_dump(fseek($h, 3, SEEK_CUR)); // 0
var_dump(ftell($h)); // 7

fclose($h);
unlink($tmp);
?>
--EXPECT--
int(-1)
int(4)
int(-1)
int(4)
int(-1)
int(4)
int(0)
int(7)
8 changes: 8 additions & 0 deletions main/streams/streams.c
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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;
Expand Down
Loading