Commit 5ce795bec19 for php.net
commit 5ce795bec1988052596c77a27dddbe4770e7487c
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Sat Jun 20 19:01:46 2026 -0400
Fix GH-20964: fseek() on php://memory with PHP_INT_MIN causes undefined behavior (#21433)
Negate after casting to unsigned instead of before, avoiding signed
integer overflow when offset is ZEND_LONG_MIN. The same pattern existed
in the pdo_sqlite and sqlite3 stream seek handlers.
Closes GH-20964
Closes GH-20927
diff --git a/ext/pdo_sqlite/pdo_sqlite.c b/ext/pdo_sqlite/pdo_sqlite.c
index 0e7e02df46a..2da9329e2a0 100644
--- a/ext/pdo_sqlite/pdo_sqlite.c
+++ b/ext/pdo_sqlite/pdo_sqlite.c
@@ -203,7 +203,7 @@ static int php_pdosqlite3_stream_seek(php_stream *stream, zend_off_t offset, int
switch(whence) {
case SEEK_CUR:
if (offset < 0) {
- if (sqlite3_stream->position < (size_t)(-offset)) {
+ if (sqlite3_stream->position < -(size_t)offset) {
sqlite3_stream->position = 0;
*newoffs = -1;
return -1;
@@ -241,7 +241,7 @@ static int php_pdosqlite3_stream_seek(php_stream *stream, zend_off_t offset, int
sqlite3_stream->position = sqlite3_stream->size;
*newoffs = -1;
return -1;
- } else if (sqlite3_stream->size < (size_t)(-offset)) {
+ } else if (sqlite3_stream->size < -(size_t)offset) {
sqlite3_stream->position = 0;
*newoffs = -1;
return -1;
diff --git a/ext/pdo_sqlite/tests/gh20964.phpt b/ext/pdo_sqlite/tests/gh20964.phpt
new file mode 100644
index 00000000000..65070cb1838
--- /dev/null
+++ b/ext/pdo_sqlite/tests/gh20964.phpt
@@ -0,0 +1,23 @@
+--TEST--
+GH-20964 (fseek() on PDO SQLite blob stream with PHP_INT_MIN causes undefined behavior)
+--EXTENSIONS--
+pdo_sqlite
+--FILE--
+<?php
+$db = new Pdo\Sqlite('sqlite::memory:');
+
+$db->exec('CREATE TABLE test (id INTEGER PRIMARY KEY, data BLOB)');
+$db->exec("INSERT INTO test (id, data) VALUES (1, 'hello')");
+
+$stream = $db->openBlob('test', 'data', 1);
+
+var_dump(fseek($stream, PHP_INT_MIN, SEEK_END));
+
+rewind($stream);
+var_dump(fseek($stream, PHP_INT_MIN, SEEK_CUR));
+
+fclose($stream);
+?>
+--EXPECT--
+int(-1)
+int(-1)
diff --git a/ext/sqlite3/sqlite3.c b/ext/sqlite3/sqlite3.c
index bef568a62df..67ff850cadd 100644
--- a/ext/sqlite3/sqlite3.c
+++ b/ext/sqlite3/sqlite3.c
@@ -1158,7 +1158,7 @@ static int php_sqlite3_stream_seek(php_stream *stream, zend_off_t offset, int wh
switch(whence) {
case SEEK_CUR:
if (offset < 0) {
- if (sqlite3_stream->position < (size_t)(-offset)) {
+ if (sqlite3_stream->position < -(size_t)offset) {
sqlite3_stream->position = 0;
*newoffs = -1;
return -1;
@@ -1199,7 +1199,7 @@ static int php_sqlite3_stream_seek(php_stream *stream, zend_off_t offset, int wh
sqlite3_stream->position = sqlite3_stream->size;
*newoffs = -1;
return -1;
- } else if (sqlite3_stream->size < (size_t)(-offset)) {
+ } else if (sqlite3_stream->size < -(size_t)offset) {
sqlite3_stream->position = 0;
*newoffs = -1;
return -1;
diff --git a/ext/sqlite3/tests/gh20964.phpt b/ext/sqlite3/tests/gh20964.phpt
new file mode 100644
index 00000000000..852940ba983
--- /dev/null
+++ b/ext/sqlite3/tests/gh20964.phpt
@@ -0,0 +1,24 @@
+--TEST--
+GH-20964 (fseek() on SQLite3 blob stream with PHP_INT_MIN causes undefined behavior)
+--EXTENSIONS--
+sqlite3
+--FILE--
+<?php
+require_once __DIR__ . '/new_db.inc';
+
+$db->exec('CREATE TABLE test (id INTEGER PRIMARY KEY, data BLOB)');
+$db->exec("INSERT INTO test (id, data) VALUES (1, 'hello')");
+
+$stream = $db->openBlob('test', 'data', 1);
+
+var_dump(fseek($stream, PHP_INT_MIN, SEEK_END));
+
+rewind($stream);
+var_dump(fseek($stream, PHP_INT_MIN, SEEK_CUR));
+
+fclose($stream);
+$db->close();
+?>
+--EXPECT--
+int(-1)
+int(-1)
diff --git a/main/streams/memory.c b/main/streams/memory.c
index af54c46dd9a..1cc1886e609 100644
--- a/main/streams/memory.c
+++ b/main/streams/memory.c
@@ -126,7 +126,7 @@ static int php_stream_memory_seek(php_stream *stream, zend_off_t offset, int whe
switch(whence) {
case SEEK_CUR:
if (offset < 0) {
- if (ms->fpos < (size_t)(-offset)) {
+ if (ms->fpos < -(size_t)offset) {
ms->fpos = 0;
*newoffs = -1;
return -1;
@@ -163,7 +163,7 @@ static int php_stream_memory_seek(php_stream *stream, zend_off_t offset, int whe
stream->eof = 0;
stream->fatal_error = 0;
return 0;
- } else if (ZSTR_LEN(ms->data) < (size_t)(-offset)) {
+ } else if (ZSTR_LEN(ms->data) < -(size_t)offset) {
ms->fpos = 0;
*newoffs = -1;
return -1;
diff --git a/tests/basic/gh20964.phpt b/tests/basic/gh20964.phpt
new file mode 100644
index 00000000000..2a97164c792
--- /dev/null
+++ b/tests/basic/gh20964.phpt
@@ -0,0 +1,22 @@
+--TEST--
+GH-20964 (fseek() on php://memory with PHP_INT_MIN causes undefined behavior)
+--FILE--
+<?php
+$stream = fopen('php://memory', 'r+');
+fwrite($stream, 'hello');
+rewind($stream);
+
+var_dump(fseek($stream, PHP_INT_MIN, SEEK_END));
+var_dump(ftell($stream));
+
+rewind($stream);
+var_dump(fseek($stream, PHP_INT_MIN, SEEK_CUR));
+var_dump(ftell($stream));
+
+fclose($stream);
+?>
+--EXPECT--
+int(-1)
+bool(false)
+int(-1)
+bool(false)