Skip to content

Fix fseek() accepting $whence values that do not fit in an int - #23483

Open
lacatoire wants to merge 2 commits into
php:masterfrom
lacatoire:fix/fseek-whence-int-narrowing
Open

Fix fseek() accepting $whence values that do not fit in an int#23483
lacatoire wants to merge 2 commits into
php:masterfrom
lacatoire:fix/fseek-whence-int-narrowing

Conversation

@lacatoire

Copy link
Copy Markdown
Member

fseek() parses $whence as a zend_long, but casts it to a C int when calling php_stream_seek(). Values whose low 32 bits alias onto a valid seek constant are accepted and acted upon.

On a 64-bit build, with a 10-byte file and the cursor at 4:

fseek($h, 3, SEEK_CUR + 2**32)  =>  0   position 7    (treated as SEEK_CUR)
fseek($h, 3, PHP_INT_MIN)       =>  0   position 3    (treated as SEEK_SET)
fseek($h, 3, SEEK_END + 2**32)  =>  0   position 13   (treated as SEEK_END)

A return value of 0 means success, so the call reports that a seek nobody asked for went through.

The fix rejects values outside the int range before the cast:

if (whence < INT_MIN || whence > INT_MAX) {
	RETURN_LONG(-1);
}

-1 is not a new convention: it is already what fseek() returns for an invalid $whence that happens to fit in an int, and the position is left untouched in both cases.

fseek($h, 3, 99)  =>  -1   position unchanged

Platform constants such as SEEK_DATA (3) and SEEK_HOLE (4) fit in an int and are unaffected.

ext/standard/tests/file/fseek_whence_overflow.phpt covers the three aliasing forms and a plain SEEK_CUR as a regression guard. It skips on 32-bit builds, where no zend_long can exceed an int.

Tested on a 64-bit Linux build: the new test passes, and ext/standard/tests/file, ext/standard/tests/streams and ext/standard/tests/filters show no regression.

Comment thread ext/standard/file.c
$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.
@lacatoire
lacatoire force-pushed the fix/fseek-whence-int-narrowing branch from 7c44b7a to df3278d Compare August 28, 2026 12:33
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.
Comment thread ext/standard/file.c
Z_PARAM_LONG(whence)
ZEND_PARSE_PARAMETERS_END();

if (whence < INT_MIN || whence > INT_MAX) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can just use ZEND_LONG_EXCEEDS_INT macro here.

@@ -0,0 +1,40 @@
--TEST--
fseek(): an invalid $whence that fits in an int must not desynchronize the stream

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would also test for non file based streams, eg. memory.

?>
--FILE--
<?php
$tmp = tempnam(sys_get_temp_dir(), 'fseek');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also you need a CLEAN section for this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants