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
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ PHP NEWS
. Allowed filtered streams to be casted as fd for select. (Jakub Zelenka)
. Fixed bug GH-21221 (Prevent closing of innerstream of php://temp stream).
(ilutov)
. Fixed bug GH-21700 (Assertion failure in _php_stream_seek when a user
stream returns a negative position). (lacatoire)
. Improved stream_socket_server() bind failure error reporting. (ilutov)
. Fixed bug #49874 (ftell() and fseek() inconsistency when using stream
filters). (Jakub Zelenka)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
--TEST--
GH-21700: Append-mode open ignores a negative position returned by a user stream
--FILE--
<?php
class lyingstream {
public function stream_open($path, $mode, $options, &$opened_path) { return true; }
public function stream_seek($offset, $whence) { return true; }
public function stream_tell(): int { return -42; }
public function stream_eof() { return false; }
public function stream_read($count) { return ''; }
}

stream_wrapper_register("test", "lyingstream");

// Opening in append mode triggers an internal SEEK_CUR to revise the initial
// position; if the user stream lies, ftell() must still report a non-negative
// value and a subsequent SEEK_CUR must not trip the position invariant.
$fp = fopen("test://foo", "ab");
var_dump(ftell($fp));
var_dump(fseek($fp, 0, SEEK_CUR));
fclose($fp);

stream_wrapper_unregister("test");
?>
--EXPECT--
int(0)
int(-1)
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
--TEST--
GH-21700: User stream returning negative position must not trigger an assertion
--FILE--
<?php
class mystream {
public int $pos = 0;
public function stream_open($path, $mode, $options, &$opened_path) { return true; }
public function stream_seek($offset, $whence) {
// Blindly store the offset, mimicking a buggy user implementation.
$this->pos = $offset;
return true;
}
public function stream_tell() { return $this->pos; }
public function stream_eof() { return false; }
public function stream_read($count) { return ''; }
}

class lyingstream extends mystream {
public function stream_tell(): int { return -42; }
}

// 1. SEEK_CUR with a negative resulting absolute position is rejected
// before invoking the user seek hook, so stream->position stays valid.
stream_wrapper_register("test", "mystream");
$fp = fopen("test://foo", "rb");
var_dump(fseek($fp, -1, SEEK_CUR));
var_dump(ftell($fp));
var_dump(fseek($fp, 0, SEEK_CUR));
fclose($fp);
stream_wrapper_unregister("test");

// 2. A user stream_tell() returning a negative position is rejected,
// keeping stream->position non-negative.
stream_wrapper_register("test", "lyingstream");
$fp = fopen("test://foo", "rb");
var_dump(fseek($fp, 100, SEEK_SET));
var_dump(ftell($fp));
var_dump(fseek($fp, 0, SEEK_CUR));
fclose($fp);
stream_wrapper_unregister("test");
?>
--EXPECT--
int(-1)
int(0)
int(0)
int(-1)
int(0)
int(-1)
22 changes: 17 additions & 5 deletions main/streams/streams.c
Original file line number Diff line number Diff line change
Expand Up @@ -1466,19 +1466,31 @@ PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence)
} else {
offset = stream->position + offset;
}
whence = SEEK_SET;
break;
whence = SEEK_SET;
ZEND_FALLTHROUGH;
case SEEK_SET:
if (offset < 0) {
return -1;
}
break;
}
zend_off_t saved_position = stream->position;
ret = stream->ops->seek(stream, offset, whence, &stream->position);

if (((stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0) || ret == 0) {
if (ret == 0) {
stream->eof = 0;
stream->fatal_error = 0;
if (UNEXPECTED(stream->position < 0)) {
/* The seek operation reported success but updated
* stream->position to a negative value (e.g. a user
* stream's stream_tell() returned a negative offset).
* Treat as failure and restore the original position
* to keep the invariant stream->position >= 0. */
stream->position = saved_position;
ret = -1;
} else {
stream->eof = 0;
stream->fatal_error = 0;
}
}

/* invalidate the buffer contents */
Expand Down Expand Up @@ -2392,7 +2404,7 @@ PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mod
zend_off_t newpos = 0;

/* if opened for append, we need to revise our idea of the initial file position */
if (0 == stream->ops->seek(stream, 0, SEEK_CUR, &newpos)) {
if (0 == stream->ops->seek(stream, 0, SEEK_CUR, &newpos) && newpos >= 0) {
stream->position = newpos;
}
}
Expand Down
Loading