Skip to content

Commit efcfd55

Browse files
committed
Fix GH-21700: Assertion failure in _php_stream_seek with user stream returning negative position
Validate that stream->position remains non-negative after a user stream's seek operation. Some user streams may report success from stream_seek() while their stream_tell() returns a negative value, leaving stream->position in an invalid state and triggering ZEND_ASSERT(stream->position >= 0) on the next SEEK_CUR. Also reject SEEK_CUR seeks whose absolute target would be negative (reusing the existing SEEK_SET negative-offset rejection via fall-through), so the user seek hook is not even invoked in that case. On invalid post-seek position, restore stream->position to its prior value and return -1, matching the POSIX fseek() contract that the file-position indicator is unchanged on failure.
1 parent 1462499 commit efcfd55

3 files changed

Lines changed: 64 additions & 4 deletions

File tree

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,8 @@ PHP NEWS
195195
. Allowed filtered streams to be casted as fd for select. (Jakub Zelenka)
196196
. Fixed bug GH-21221 (Prevent closing of innerstream of php://temp stream).
197197
(ilutov)
198+
. Fixed bug GH-21700 (Assertion failure in _php_stream_seek when a user
199+
stream returns a negative position). (lacatoire)
198200
. Improved stream_socket_server() bind failure error reporting. (ilutov)
199201
. Fixed bug #49874 (ftell() and fseek() inconsistency when using stream
200202
filters). (Jakub Zelenka)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
--TEST--
2+
GH-21700: User stream returning negative position must not trigger an assertion
3+
--FILE--
4+
<?php
5+
class mystream {
6+
public int $pos = 0;
7+
public function stream_open($path, $mode, $options, &$opened_path) { return true; }
8+
public function stream_seek($offset, $whence) {
9+
// Blindly store the offset, mimicking a buggy user implementation.
10+
$this->pos = $offset;
11+
return true;
12+
}
13+
public function stream_tell() { return $this->pos; }
14+
public function stream_eof() { return false; }
15+
public function stream_read($count) { return ''; }
16+
}
17+
stream_wrapper_register("test", "mystream");
18+
19+
// 1. SEEK_CUR with a negative resulting absolute position is rejected.
20+
$fp = fopen("test://foo", "rb");
21+
var_dump(fseek($fp, -1, SEEK_CUR));
22+
var_dump(ftell($fp));
23+
var_dump(fseek($fp, 0, SEEK_CUR));
24+
fclose($fp);
25+
26+
// 2. A user stream_tell() returning a negative position is rejected,
27+
// keeping stream->position non-negative.
28+
class lyingstream extends mystream {
29+
public function stream_tell(): int { return -42; }
30+
}
31+
stream_wrapper_unregister("test");
32+
stream_wrapper_register("test", "lyingstream");
33+
34+
$fp = fopen("test://foo", "rb");
35+
var_dump(fseek($fp, 100, SEEK_SET));
36+
var_dump(ftell($fp));
37+
var_dump(fseek($fp, 0, SEEK_CUR));
38+
fclose($fp);
39+
?>
40+
--EXPECT--
41+
int(-1)
42+
int(0)
43+
int(0)
44+
int(-1)
45+
int(0)
46+
int(-1)

main/streams/streams.c

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1466,19 +1466,31 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence)
14661466
} else {
14671467
offset = stream->position + offset;
14681468
}
1469-
whence = SEEK_SET;
1470-
break;
1469+
whence = SEEK_SET;
1470+
ZEND_FALLTHROUGH;
14711471
case SEEK_SET:
14721472
if (offset < 0) {
14731473
return -1;
14741474
}
1475+
break;
14751476
}
1477+
zend_off_t saved_position = stream->position;
14761478
ret = stream->ops->seek(stream, offset, whence, &stream->position);
14771479

14781480
if (((stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0) || ret == 0) {
14791481
if (ret == 0) {
1480-
stream->eof = 0;
1481-
stream->fatal_error = 0;
1482+
if (UNEXPECTED(stream->position < 0)) {
1483+
/* The seek operation reported success but updated
1484+
* stream->position to a negative value (e.g. a user
1485+
* stream's stream_tell() returned a negative offset).
1486+
* Treat as failure and restore the original position
1487+
* to keep the invariant stream->position >= 0. */
1488+
stream->position = saved_position;
1489+
ret = -1;
1490+
} else {
1491+
stream->eof = 0;
1492+
stream->fatal_error = 0;
1493+
}
14821494
}
14831495

14841496
/* invalidate the buffer contents */

0 commit comments

Comments
 (0)