From 4af78f0fc69ec6aa2cdb2e984ead16c1b9957b7d Mon Sep 17 00:00:00 2001 From: Marc Bennewitz Date: Fri, 25 Sep 2026 06:50:12 +0200 Subject: [PATCH 1/4] Fix failed seek position on php://memory A failed seek reset the internal position to 0 but reported -1 as the stream position, so ftell() returned false and a following SEEK_CUR tripped an assertion. Leave the position unchanged instead, like plain files. php://temp was affected too while its data is held in memory, as it forwards seeks to an inner php://memory stream. Once spilled to a temporary file it already behaved correctly. Its seek no longer reports -1 either when it has no inner stream. --- NEWS | 4 + UPGRADING | 5 + ext/standard/tests/streams/gh23905.phpt | 295 ++++++++++++++++++++++++ main/streams/memory.c | 21 +- tests/basic/gh20964.phpt | 4 +- 5 files changed, 308 insertions(+), 21 deletions(-) create mode 100644 ext/standard/tests/streams/gh23905.phpt diff --git a/NEWS b/NEWS index 191a241cb246..52f326d9c2bf 100644 --- a/NEWS +++ b/NEWS @@ -122,6 +122,10 @@ PHP NEWS . Fixed bug GH-23899 (Assertion failure when a cancel callback returns an invalid type during shutdown). (Weilin Du) +- Streams: + . Fixed bug GH-23905 (Failed seek on php://memory and php://temp streams + sets the stream position to -1). (Marc Bennewitz) + 24 Sep 2026, PHP 8.6.0RC2 - Core: diff --git a/UPGRADING b/UPGRADING index 0e4a5288d680..8b5d7e92f4c1 100644 --- a/UPGRADING +++ b/UPGRADING @@ -320,6 +320,11 @@ PHP 8.6 UPGRADE NOTES when a repeater precedes it, as in "s1 +--EXPECT-- +php://memory +-- SEEK_SET -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(0) +bool(false) +int(15) +int(0) +int(0) +string(11) "hello world" +bool(true) +-- SEEK_CUR -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(0) +bool(false) +int(15) +int(0) +int(0) +string(11) "hello world" +bool(true) +-- SEEK_END -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(0) +bool(false) +int(15) +int(0) +int(0) +string(11) "hello world" +bool(true) + + +php://filter/string.rot13/resource=php://memory +-- SEEK_SET -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(0) +bool(false) +int(15) +int(0) +int(0) +string(11) "hello world" +bool(true) +-- SEEK_CUR -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(0) +bool(false) +int(15) +int(0) +int(0) +string(11) "hello world" +bool(true) +-- SEEK_END -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(0) +bool(false) +int(15) +int(0) +int(0) +string(11) "hello world" +bool(true) + + +php://temp +-- SEEK_SET -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(0) +bool(false) +int(15) +int(0) +int(0) +string(11) "hello world" +bool(true) +-- SEEK_CUR -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(0) +bool(false) +int(15) +int(0) +int(0) +string(11) "hello world" +bool(true) +-- SEEK_END -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(0) +bool(false) +int(15) +int(0) +int(0) +string(11) "hello world" +bool(true) + + +php://temp/maxmemory:0 +-- SEEK_SET -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(0) +bool(false) +int(15) +int(0) +int(0) +string(11) "hello world" +bool(true) +-- SEEK_CUR -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(0) +bool(false) +int(15) +int(0) +int(0) +string(11) "hello world" +bool(true) +-- SEEK_END -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(0) +bool(false) +int(15) +int(0) +int(0) +string(11) "hello world" +bool(true) + + +php://filter/string.rot13/resource=php://temp +-- SEEK_SET -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(0) +bool(false) +int(15) +int(0) +int(0) +string(11) "hello world" +bool(true) +-- SEEK_CUR -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(0) +bool(false) +int(15) +int(0) +int(0) +string(11) "hello world" +bool(true) +-- SEEK_END -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(0) +bool(false) +int(15) +int(0) +int(0) +string(11) "hello world" +bool(true) + + +php://filter/string.rot13/resource=php://temp/maxmemory:0 +-- SEEK_SET -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(0) +bool(false) +int(15) +int(0) +int(0) +string(11) "hello world" +bool(true) +-- SEEK_CUR -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(0) +bool(false) +int(15) +int(0) +int(0) +string(11) "hello world" +bool(true) +-- SEEK_END -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(0) +bool(false) +int(15) +int(0) +int(0) +string(11) "hello world" +bool(true) diff --git a/main/streams/memory.c b/main/streams/memory.c index e76598ed0f46..9a161a96d7ab 100644 --- a/main/streams/memory.c +++ b/main/streams/memory.c @@ -127,55 +127,38 @@ static int php_stream_memory_seek(php_stream *stream, zend_off_t offset, int whe case SEEK_CUR: if (offset < 0) { if (ms->fpos < -(size_t)offset) { - ms->fpos = 0; - *newoffs = -1; return -1; } else { ms->fpos = ms->fpos + offset; *newoffs = ms->fpos; - stream->eof = 0; - stream->fatal_error = 0; return 0; } } else { - stream->eof = 0; - stream->fatal_error = 0; ms->fpos = ms->fpos + offset; *newoffs = ms->fpos; return 0; } case SEEK_SET: if (offset < 0) { - ms->fpos = 0; - *newoffs = -1; return -1; } else { ms->fpos = offset; *newoffs = ms->fpos; - stream->eof = 0; - stream->fatal_error = 0; return 0; } case SEEK_END: if (offset > 0) { ms->fpos = ZSTR_LEN(ms->data) + offset; *newoffs = ms->fpos; - stream->eof = 0; - stream->fatal_error = 0; return 0; } else if (ZSTR_LEN(ms->data) < -(size_t)offset) { - ms->fpos = 0; - *newoffs = -1; return -1; } else { ms->fpos = ZSTR_LEN(ms->data) + offset; *newoffs = ms->fpos; - stream->eof = 0; - stream->fatal_error = 0; return 0; } default: - *newoffs = ms->fpos; return -1; } } @@ -446,10 +429,10 @@ static int php_stream_temp_seek(php_stream *stream, zend_off_t offset, int whenc assert(ts != NULL); - if (!ts->innerstream) { - *newoffs = -1; + if (UNEXPECTED(!ts->innerstream)) { return -1; } + ret = php_stream_seek(ts->innerstream, offset, whence); *newoffs = php_stream_tell(ts->innerstream); stream->eof = ts->innerstream->eof; diff --git a/tests/basic/gh20964.phpt b/tests/basic/gh20964.phpt index 2a97164c7924..705e59db3a6f 100644 --- a/tests/basic/gh20964.phpt +++ b/tests/basic/gh20964.phpt @@ -17,6 +17,6 @@ fclose($stream); ?> --EXPECT-- int(-1) -bool(false) +int(0) int(-1) -bool(false) +int(0) From e1a3a20bac505aa3d628cd3ffd9b22f02393e4be Mon Sep 17 00:00:00 2001 From: Marc Bennewitz Date: Fri, 2 Oct 2026 07:28:35 +0200 Subject: [PATCH 2/4] Fix failed seek discarding the read buffer A failed seek discarded the read buffer although the stream did not move. A buffered stream has usually read ahead, so a following read or write continued from where the stream had read ahead to, while ftell() still reported the old position. Return early instead, keeping the position, the read buffer and the filter state. --- NEWS | 2 + UPGRADING | 4 + UPGRADING.INTERNALS | 3 + .../stream_seek_failure_keeps_buffer.phpt | 103 ++++++++++++++++++ main/streams/streams.c | 13 ++- 5 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 ext/standard/tests/streams/stream_seek_failure_keeps_buffer.phpt diff --git a/NEWS b/NEWS index 52f326d9c2bf..df37de96096c 100644 --- a/NEWS +++ b/NEWS @@ -125,6 +125,8 @@ PHP NEWS - Streams: . Fixed bug GH-23905 (Failed seek on php://memory and php://temp streams sets the stream position to -1). (Marc Bennewitz) + . Fixed a failed seek discarding the read buffer of buffered streams. + (Marc Bennewitz) 24 Sep 2026, PHP 8.6.0RC2 diff --git a/UPGRADING b/UPGRADING index 8b5d7e92f4c1..f73f991d5dd4 100644 --- a/UPGRADING +++ b/UPGRADING @@ -324,6 +324,10 @@ PHP 8.6 UPGRADE NOTES . A failed fseek() on php://memory and php://temp streams now leaves the stream position unchanged. Previously, ftell() returned false afterwards and the stream was moved to its start. + . A failed fseek() on buffered streams, such as plain files, now leaves the + position of the next read or write unchanged. Previously, the read buffer + was discarded, so the next read skipped the buffered data and the next + write happened where the stream had read ahead to. - Sysvshm: . shm_attach() now raises a ValueError when the $key argument is outside the diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index 255d4c2ca893..82047977c845 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -167,6 +167,9 @@ PHP 8.6 INTERNALS UPGRADE NOTES . EG(in_autoload) was renamed to EG(autoload_current_classnames) and no longer is a pointer, but a directly embedded HashTable struct. . Extended php_stream_filter_ops with seek method. + . php_stream_seek() no longer discards the read buffer when the seek handler + of php_stream_ops fails. A seek handler must therefore leave both its own + position and the returned new offset unchanged on failure. . php_print_info_htmlhead() now takes a title argument. . zend_argument_error_variadic() now takes a new 'function' parameter. . The param argument in the php_verror() function has been removed. diff --git a/ext/standard/tests/streams/stream_seek_failure_keeps_buffer.phpt b/ext/standard/tests/streams/stream_seek_failure_keeps_buffer.phpt new file mode 100644 index 000000000000..1c9fd6450441 --- /dev/null +++ b/ext/standard/tests/streams/stream_seek_failure_keeps_buffer.phpt @@ -0,0 +1,103 @@ +--TEST-- +A failed seek keeps the stream position and the read buffer +--FILE-- +data, $this->pos, $count); + $this->pos += strlen($ret); + return $ret; + } + + public function stream_eof(): bool { + return $this->pos >= strlen($this->data); + } + + public function stream_tell(): int { + return $this->pos; + } + + public function stream_seek(int $offset, int $whence): bool { + $pos = match ($whence) { + SEEK_SET => $offset, + SEEK_CUR => $this->pos + $offset, + SEEK_END => strlen($this->data) + $offset, + }; + if ($pos < 0 || $pos > strlen($this->data)) { + return false; + } + $this->pos = $pos; + return true; + } +} +stream_wrapper_register('test', TestStream::class); + +$file = __DIR__ . '/stream_seek_failure_keeps_buffer.txt'; +file_put_contents($file, 'hello world'); + +echo "-- plain file --\n"; +$stream = fopen($file, 'r'); +var_dump(fread($stream, 5)); +var_dump(fseek($stream, -6, SEEK_CUR), ftell($stream), feof($stream)); +var_dump(fseek($stream, -12, SEEK_END), ftell($stream), feof($stream)); +var_dump(fread($stream, 10), feof($stream)); +fclose($stream); + +echo "-- plain file, write after failed seek --\n"; +$stream = fopen($file, 'r+'); +var_dump(fread($stream, 5)); +var_dump(fseek($stream, -12, SEEK_END), fwrite($stream, '!'), ftell($stream)); +fclose($stream); +var_dump(file_get_contents($file)); + +echo "-- user stream --\n"; +$stream = fopen('test://', 'r'); +var_dump(fread($stream, 5)); +var_dump(fseek($stream, -6, SEEK_CUR), ftell($stream), feof($stream)); +var_dump(fseek($stream, 12, SEEK_SET), ftell($stream), feof($stream)); +var_dump(fseek($stream, 1, SEEK_END), ftell($stream), feof($stream)); +var_dump(fread($stream, 10), feof($stream)); +fclose($stream); +?> +--CLEAN-- + +--EXPECT-- +-- plain file -- +string(5) "hello" +int(-1) +int(5) +bool(false) +int(-1) +int(5) +bool(false) +string(6) " world" +bool(true) +-- plain file, write after failed seek -- +string(5) "hello" +int(-1) +int(1) +int(6) +string(11) "hello!world" +-- user stream -- +string(5) "hello" +int(-1) +int(5) +bool(false) +int(-1) +int(5) +bool(false) +int(-1) +int(5) +bool(false) +string(6) " world" +bool(true) diff --git a/main/streams/streams.c b/main/streams/streams.c index ae2d4b910627..3d783bd2efea 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -1402,17 +1402,20 @@ 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->eof = 0; - stream->fatal_error = 0; - } + if (ret == 0) { + stream->eof = 0; + stream->fatal_error = 0; /* invalidate the buffer contents */ stream->readpos = stream->writepos = 0; return php_stream_filters_seek_all(stream, is_start_seeking, offset, whence) == SUCCESS ? ret : -1; } + + if ((stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0) { + /* the stream did not move, so the position and the buffer contents are still valid */ + return ret; + } /* else the stream has decided that it can't support seeking after all; * fall through to attempt emulation */ } From d5fdafb51f8c240e146dd40e34ce9121012ba910 Mon Sep 17 00:00:00 2001 From: Marc Bennewitz Date: Fri, 25 Sep 2026 06:52:37 +0200 Subject: [PATCH 3/4] Fix failed seek position on SQLite3 blob streams A failed seek clamped the internal position to 0 or the blob size but reported -1 as the stream position, so ftell() returned false and a following SEEK_CUR tripped an assertion. Leave both positions unchanged instead, and reject a negative SEEK_SET offset explicitly rather than relying on the size_t cast. --- NEWS | 4 + UPGRADING | 7 ++ ext/sqlite3/sqlite3.c | 13 +-- ext/sqlite3/tests/gh23905.phpt | 149 +++++++++++++++++++++++++++++++++ 4 files changed, 161 insertions(+), 12 deletions(-) create mode 100644 ext/sqlite3/tests/gh23905.phpt diff --git a/NEWS b/NEWS index df37de96096c..05a068ce912d 100644 --- a/NEWS +++ b/NEWS @@ -122,6 +122,10 @@ PHP NEWS . Fixed bug GH-23899 (Assertion failure when a cancel callback returns an invalid type during shutdown). (Weilin Du) +- SQLite3: + . Fixed bug GH-23905 (Failed seek on SQLite3 blob streams sets the stream + position to -1). (Marc Bennewitz) + - Streams: . Fixed bug GH-23905 (Failed seek on php://memory and php://temp streams sets the stream position to -1). (Marc Bennewitz) diff --git a/UPGRADING b/UPGRADING index f73f991d5dd4..2f3f32131fad 100644 --- a/UPGRADING +++ b/UPGRADING @@ -246,6 +246,13 @@ PHP 8.6 UPGRADE NOTES . DirectoryIterator::key() now returns int|string, and DirectoryIterator::current() returns string|SplFileInfo|static. +- SQLite3: + . A failed fseek() on a blob stream opened by SQLite3::openBlob() now leaves + the stream position unchanged. Previously, ftell() returned false + afterwards and the stream was moved to the start or the end of the blob. + As a result, an fwrite() after a failed seek past the end now writes at + the unchanged position instead of failing. + - Standard: . array_intersect() with at least two arrays now converts values to strings while scanning its inputs instead of during sort comparisons. This can diff --git a/ext/sqlite3/sqlite3.c b/ext/sqlite3/sqlite3.c index 4698fe5cd096..b2d47378e1f7 100644 --- a/ext/sqlite3/sqlite3.c +++ b/ext/sqlite3/sqlite3.c @@ -1201,8 +1201,6 @@ static int php_sqlite3_stream_seek(php_stream *stream, zend_off_t offset, int wh case SEEK_CUR: if (offset < 0) { if (sqlite3_stream->position < -(size_t)offset) { - sqlite3_stream->position = 0; - *newoffs = -1; return -1; } else { sqlite3_stream->position = sqlite3_stream->position + offset; @@ -1213,8 +1211,6 @@ static int php_sqlite3_stream_seek(php_stream *stream, zend_off_t offset, int wh } } else { if (sqlite3_stream->position + (size_t)(offset) > sqlite3_stream->size) { - sqlite3_stream->position = sqlite3_stream->size; - *newoffs = -1; return -1; } else { sqlite3_stream->position = sqlite3_stream->position + offset; @@ -1225,9 +1221,7 @@ static int php_sqlite3_stream_seek(php_stream *stream, zend_off_t offset, int wh } } case SEEK_SET: - if (sqlite3_stream->size < (size_t)(offset)) { - sqlite3_stream->position = sqlite3_stream->size; - *newoffs = -1; + if (offset < 0 || (size_t)offset > sqlite3_stream->size) { return -1; } else { sqlite3_stream->position = offset; @@ -1238,12 +1232,8 @@ static int php_sqlite3_stream_seek(php_stream *stream, zend_off_t offset, int wh } case SEEK_END: if (offset > 0) { - sqlite3_stream->position = sqlite3_stream->size; - *newoffs = -1; return -1; } else if (sqlite3_stream->size < -(size_t)offset) { - sqlite3_stream->position = 0; - *newoffs = -1; return -1; } else { sqlite3_stream->position = sqlite3_stream->size + offset; @@ -1253,7 +1243,6 @@ static int php_sqlite3_stream_seek(php_stream *stream, zend_off_t offset, int wh return 0; } default: - *newoffs = sqlite3_stream->position; return -1; } } diff --git a/ext/sqlite3/tests/gh23905.phpt b/ext/sqlite3/tests/gh23905.phpt new file mode 100644 index 000000000000..957683f248c9 --- /dev/null +++ b/ext/sqlite3/tests/gh23905.phpt @@ -0,0 +1,149 @@ +--TEST-- +GH-23905 (Failed seek on SQLite3 blob streams sets the stream position to -1) +--EXTENSIONS-- +sqlite3 +--FILE-- +exec('CREATE TABLE test (id INTEGER PRIMARY KEY, data BLOB)'); +$db->exec("INSERT INTO test (id, data) VALUES (1, x'68656c6c6f20776f726c64')"); // "hello world" + +foreach (['read-only' => SQLITE3_OPEN_READONLY, 'read-write' => SQLITE3_OPEN_READWRITE] as $mode => $flags) { + echo $mode, "\n"; + $stream = $db->openBlob('test', 'data', 1, 'main', $flags); + + echo "-- SEEK_SET --\n"; + fseek($stream, 6); + var_dump(fseek($stream, -1, SEEK_SET), ftell($stream)); + var_dump(fseek($stream, -6, SEEK_CUR), ftell($stream), fread($stream, 5)); + var_dump(fseek($stream, -1, SEEK_SET), ftell($stream)); + var_dump(fseek($stream, 12, SEEK_SET), feof($stream), ftell($stream)); + var_dump(fread($stream, 10), feof($stream)); + + echo "-- SEEK_CUR --\n"; + fseek($stream, 6); + var_dump(fseek($stream, -7, SEEK_CUR), ftell($stream)); + var_dump(fseek($stream, -6, SEEK_CUR), ftell($stream), fread($stream, 5)); + var_dump(fseek($stream, -6, SEEK_CUR), ftell($stream)); + var_dump(fseek($stream, 7, SEEK_CUR), feof($stream), ftell($stream)); + var_dump(fread($stream, 10), feof($stream)); + + echo "-- SEEK_END --\n"; + fseek($stream, 6); + var_dump(fseek($stream, -12, SEEK_END), ftell($stream)); + var_dump(fseek($stream, -6, SEEK_CUR), ftell($stream), fread($stream, 5)); + var_dump(fseek($stream, -12, SEEK_END), ftell($stream)); + var_dump(fseek($stream, 1, SEEK_END), feof($stream), ftell($stream)); + var_dump(fread($stream, 10), feof($stream)); + + if ($flags === SQLITE3_OPEN_READWRITE) { + // A blob cannot grow, so a write after a failed seek past the end + // writes at the unchanged position + echo "-- write after failed seek --\n"; + fseek($stream, 6); + var_dump(fseek($stream, 12, SEEK_SET), fwrite($stream, 'W'), ftell($stream)); + fseek($stream, 0); + var_dump(fread($stream, 5)); + var_dump(fseek($stream, 1, SEEK_END), fwrite($stream, '!'), ftell($stream)); + var_dump(fseek($stream, 0), fread($stream, 15)); + } + + fclose($stream); + echo "\n"; +} + +$db->close(); +?> +--EXPECT-- +read-only +-- SEEK_SET -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(-1) +int(5) +int(-1) +bool(false) +int(5) +string(6) " world" +bool(true) +-- SEEK_CUR -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(-1) +int(5) +int(-1) +bool(false) +int(5) +string(6) " world" +bool(true) +-- SEEK_END -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(-1) +int(5) +int(-1) +bool(false) +int(5) +string(6) " world" +bool(true) + +read-write +-- SEEK_SET -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(-1) +int(5) +int(-1) +bool(false) +int(5) +string(6) " world" +bool(true) +-- SEEK_CUR -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(-1) +int(5) +int(-1) +bool(false) +int(5) +string(6) " world" +bool(true) +-- SEEK_END -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(-1) +int(5) +int(-1) +bool(false) +int(5) +string(6) " world" +bool(true) +-- write after failed seek -- +int(-1) +int(1) +int(7) +string(5) "hello" +int(-1) +int(1) +int(6) +int(0) +string(11) "hello!World" From bc78826bb235e1e97cd1409dfac2d7568e118bf8 Mon Sep 17 00:00:00 2001 From: Marc Bennewitz Date: Fri, 25 Sep 2026 06:54:05 +0200 Subject: [PATCH 4/4] Fix failed seek position on PDO SQLite blob streams A failed seek clamped the internal position to 0 or the blob size but reported -1 as the stream position, so ftell() returned false and a following SEEK_CUR tripped an assertion. Leave both positions unchanged instead, and reject a negative SEEK_SET offset explicitly rather than relying on the size_t cast. --- NEWS | 4 + UPGRADING | 7 ++ ext/pdo_sqlite/pdo_sqlite.c | 13 +-- ext/pdo_sqlite/tests/gh23905.phpt | 147 ++++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+), 12 deletions(-) create mode 100644 ext/pdo_sqlite/tests/gh23905.phpt diff --git a/NEWS b/NEWS index 05a068ce912d..6f230a679f15 100644 --- a/NEWS +++ b/NEWS @@ -122,6 +122,10 @@ PHP NEWS . Fixed bug GH-23899 (Assertion failure when a cancel callback returns an invalid type during shutdown). (Weilin Du) +- PDO_SQLite: + . Fixed bug GH-23905 (Failed seek on PDO SQLite blob streams sets the stream + position to -1). (Marc Bennewitz) + - SQLite3: . Fixed bug GH-23905 (Failed seek on SQLite3 blob streams sets the stream position to -1). (Marc Bennewitz) diff --git a/UPGRADING b/UPGRADING index 2f3f32131fad..1e9c80d3ef2a 100644 --- a/UPGRADING +++ b/UPGRADING @@ -120,6 +120,13 @@ PHP 8.6 UPGRADE NOTES execution error occurs (e.g. malformed UTF-8 input with the /u modifier). This is consistent with other preg_* functions. +- PDO_SQLite: + . A failed fseek() on a blob stream opened by Pdo\Sqlite::openBlob() now + leaves the stream position unchanged. Previously, ftell() returned false + afterwards and the stream was moved to the start or the end of the blob. + As a result, an fwrite() after a failed seek past the end now writes at + the unchanged position instead of failing. + - PGSQL: . pg_fetch_object() now reports the ValueError for a non-empty $constructor_args on a class without a constructor on the diff --git a/ext/pdo_sqlite/pdo_sqlite.c b/ext/pdo_sqlite/pdo_sqlite.c index 2da9329e2a04..f00a72e36426 100644 --- a/ext/pdo_sqlite/pdo_sqlite.c +++ b/ext/pdo_sqlite/pdo_sqlite.c @@ -204,8 +204,6 @@ static int php_pdosqlite3_stream_seek(php_stream *stream, zend_off_t offset, int case SEEK_CUR: if (offset < 0) { if (sqlite3_stream->position < -(size_t)offset) { - sqlite3_stream->position = 0; - *newoffs = -1; return -1; } else { sqlite3_stream->position = sqlite3_stream->position + offset; @@ -215,8 +213,6 @@ static int php_pdosqlite3_stream_seek(php_stream *stream, zend_off_t offset, int } } else { if (sqlite3_stream->position + (size_t)(offset) > sqlite3_stream->size) { - sqlite3_stream->position = sqlite3_stream->size; - *newoffs = -1; return -1; } else { sqlite3_stream->position = sqlite3_stream->position + offset; @@ -226,9 +222,7 @@ static int php_pdosqlite3_stream_seek(php_stream *stream, zend_off_t offset, int } } case SEEK_SET: - if (sqlite3_stream->size < (size_t)(offset)) { - sqlite3_stream->position = sqlite3_stream->size; - *newoffs = -1; + if (offset < 0 || (size_t)offset > sqlite3_stream->size) { return -1; } else { sqlite3_stream->position = offset; @@ -238,12 +232,8 @@ static int php_pdosqlite3_stream_seek(php_stream *stream, zend_off_t offset, int } case SEEK_END: if (offset > 0) { - sqlite3_stream->position = sqlite3_stream->size; - *newoffs = -1; return -1; } else if (sqlite3_stream->size < -(size_t)offset) { - sqlite3_stream->position = 0; - *newoffs = -1; return -1; } else { sqlite3_stream->position = sqlite3_stream->size + offset; @@ -252,7 +242,6 @@ static int php_pdosqlite3_stream_seek(php_stream *stream, zend_off_t offset, int return 0; } default: - *newoffs = sqlite3_stream->position; return -1; } } diff --git a/ext/pdo_sqlite/tests/gh23905.phpt b/ext/pdo_sqlite/tests/gh23905.phpt new file mode 100644 index 000000000000..84608b7a2ddb --- /dev/null +++ b/ext/pdo_sqlite/tests/gh23905.phpt @@ -0,0 +1,147 @@ +--TEST-- +GH-23905 (Failed seek on PDO SQLite blob streams sets the stream position to -1) +--EXTENSIONS-- +pdo_sqlite +--FILE-- +exec('CREATE TABLE test (id INTEGER PRIMARY KEY, data BLOB)'); +$db->exec("INSERT INTO test (id, data) VALUES (1, x'68656c6c6f20776f726c64')"); // "hello world" + +foreach (['read-only' => Pdo\Sqlite::OPEN_READONLY, 'read-write' => Pdo\Sqlite::OPEN_READWRITE] as $mode => $flags) { + echo $mode, "\n"; + $stream = $db->openBlob('test', 'data', 1, 'main', $flags); + + echo "-- SEEK_SET --\n"; + fseek($stream, 6); + var_dump(fseek($stream, -1, SEEK_SET), ftell($stream)); + var_dump(fseek($stream, -6, SEEK_CUR), ftell($stream), fread($stream, 5)); + var_dump(fseek($stream, -1, SEEK_SET), ftell($stream)); + var_dump(fseek($stream, 12, SEEK_SET), feof($stream), ftell($stream)); + var_dump(fread($stream, 10), feof($stream)); + + echo "-- SEEK_CUR --\n"; + fseek($stream, 6); + var_dump(fseek($stream, -7, SEEK_CUR), ftell($stream)); + var_dump(fseek($stream, -6, SEEK_CUR), ftell($stream), fread($stream, 5)); + var_dump(fseek($stream, -6, SEEK_CUR), ftell($stream)); + var_dump(fseek($stream, 7, SEEK_CUR), feof($stream), ftell($stream)); + var_dump(fread($stream, 10), feof($stream)); + + echo "-- SEEK_END --\n"; + fseek($stream, 6); + var_dump(fseek($stream, -12, SEEK_END), ftell($stream)); + var_dump(fseek($stream, -6, SEEK_CUR), ftell($stream), fread($stream, 5)); + var_dump(fseek($stream, -12, SEEK_END), ftell($stream)); + var_dump(fseek($stream, 1, SEEK_END), feof($stream), ftell($stream)); + var_dump(fread($stream, 10), feof($stream)); + + if ($flags === Pdo\Sqlite::OPEN_READWRITE) { + // A blob cannot grow, so a write after a failed seek past the end + // writes at the unchanged position + echo "-- write after failed seek --\n"; + fseek($stream, 6); + var_dump(fseek($stream, 12, SEEK_SET), fwrite($stream, 'W'), ftell($stream)); + fseek($stream, 0); + var_dump(fread($stream, 5)); + var_dump(fseek($stream, 1, SEEK_END), fwrite($stream, '!'), ftell($stream)); + var_dump(fseek($stream, 0), fread($stream, 15)); + } + + fclose($stream); + echo "\n"; +} +?> +--EXPECT-- +read-only +-- SEEK_SET -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(-1) +int(5) +int(-1) +bool(false) +int(5) +string(6) " world" +bool(true) +-- SEEK_CUR -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(-1) +int(5) +int(-1) +bool(false) +int(5) +string(6) " world" +bool(true) +-- SEEK_END -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(-1) +int(5) +int(-1) +bool(false) +int(5) +string(6) " world" +bool(true) + +read-write +-- SEEK_SET -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(-1) +int(5) +int(-1) +bool(false) +int(5) +string(6) " world" +bool(true) +-- SEEK_CUR -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(-1) +int(5) +int(-1) +bool(false) +int(5) +string(6) " world" +bool(true) +-- SEEK_END -- +int(-1) +int(6) +int(0) +int(0) +string(5) "hello" +int(-1) +int(5) +int(-1) +bool(false) +int(5) +string(6) " world" +bool(true) +-- write after failed seek -- +int(-1) +int(1) +int(7) +string(5) "hello" +int(-1) +int(1) +int(6) +int(0) +string(11) "hello!World"