Skip to content
Merged
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
11 changes: 11 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,24 @@ PHP NEWS
. Added Pdo\Pgsql::ATTR_CHUNK_SIZE to fetch a result set in chunks of the
given number of rows. (KentarouTakeda)

- PGSQL:
. Fixed the pg_insert(), pg_update() and pg_delete() flag error messages,
which did not name the set of flags actually accepted. (lacatoire)

- Phar:
. Fixed bug GH-23418 (Use-after-free when looking up mounted directories).
(Weilin Du)
. Fixed bug GH-23477 (Memory leak on duplicate native Phar manifest entries).
(Weilin Du)

- Standard:
. Fixed a segfault when a stream filter callback unsets StreamBucket::$data
before re-attaching the bucket. (iliaal)
. Fixed an out-of-bounds read when following a redirect response with an
empty Location header. (iliaal)
. Fixed read buffer compaction in php_stream_filter_flush(). (crystarm)
. Io\Poll\Context::wait() now rejects a $maxEvents value greater than
INT_MAX instead of truncating it. (marc-mabe)


27 Aug 2026, PHP 8.6.0beta2
Expand Down
14 changes: 8 additions & 6 deletions ext/pgsql/pgsql.c
Original file line number Diff line number Diff line change
Expand Up @@ -5740,8 +5740,9 @@ PHP_FUNCTION(pg_insert)
}

if (option & ~(PGSQL_CONV_OPTS|PGSQL_DML_NO_CONV|PGSQL_DML_EXEC|PGSQL_DML_ASYNC|PGSQL_DML_STRING|PGSQL_DML_ESCAPE)) {
zend_argument_value_error(4, "must be a valid bit mask of PGSQL_CONV_FORCE_NULL, PGSQL_DML_NO_CONV, "
"PGSQL_DML_ESCAPE, PGSQL_DML_EXEC, PGSQL_DML_ASYNC, and PGSQL_DML_STRING");
zend_argument_value_error(4, "must be a valid bit mask of PGSQL_CONV_IGNORE_DEFAULT, PGSQL_CONV_FORCE_NULL, "
"PGSQL_CONV_IGNORE_NOT_NULL, PGSQL_DML_NO_CONV, PGSQL_DML_ESCAPE, PGSQL_DML_EXEC, PGSQL_DML_ASYNC, "
"and PGSQL_DML_STRING");
RETURN_THROWS();
}

Expand Down Expand Up @@ -5972,8 +5973,9 @@ PHP_FUNCTION(pg_update)
}

if (option & ~(PGSQL_CONV_OPTS|PGSQL_DML_NO_CONV|PGSQL_DML_EXEC|PGSQL_DML_STRING|PGSQL_DML_ESCAPE)) {
zend_argument_value_error(5, "must be a valid bit mask of PGSQL_CONV_FORCE_NULL, PGSQL_DML_NO_CONV, "
"PGSQL_DML_ESCAPE, PGSQL_DML_EXEC, PGSQL_DML_ASYNC, and PGSQL_DML_STRING");
zend_argument_value_error(5, "must be a valid bit mask of PGSQL_CONV_IGNORE_DEFAULT, PGSQL_CONV_FORCE_NULL, "
"PGSQL_CONV_IGNORE_NOT_NULL, PGSQL_DML_NO_CONV, PGSQL_DML_ESCAPE, PGSQL_DML_EXEC, "
"and PGSQL_DML_STRING");
RETURN_THROWS();
}

Expand Down Expand Up @@ -6004,7 +6006,7 @@ PHP_PGSQL_API zend_result php_pgsql_delete(PGconn *pg_link, const zend_string *t
ZEND_ASSERT(pg_link != NULL);
ZEND_ASSERT(table != NULL);
ZEND_ASSERT(Z_TYPE_P(ids_array) == IS_ARRAY);
ZEND_ASSERT(!(opt & ~(PGSQL_CONV_FORCE_NULL|PGSQL_DML_EXEC|PGSQL_DML_STRING|PGSQL_DML_ESCAPE)));
ZEND_ASSERT(!(opt & ~(PGSQL_CONV_FORCE_NULL|PGSQL_DML_NO_CONV|PGSQL_DML_EXEC|PGSQL_DML_STRING|PGSQL_DML_ESCAPE)));

if (zend_hash_num_elements(Z_ARRVAL_P(ids_array)) == 0) {
return FAILURE;
Expand Down Expand Up @@ -6074,7 +6076,7 @@ PHP_FUNCTION(pg_delete)

if (option & ~(PGSQL_CONV_FORCE_NULL|PGSQL_DML_NO_CONV|PGSQL_DML_EXEC|PGSQL_DML_STRING|PGSQL_DML_ESCAPE)) {
zend_argument_value_error(4, "must be a valid bit mask of PGSQL_CONV_FORCE_NULL, PGSQL_DML_NO_CONV, "
"PGSQL_DML_ESCAPE, PGSQL_DML_EXEC, PGSQL_DML_ASYNC, and PGSQL_DML_STRING");
"PGSQL_DML_ESCAPE, PGSQL_DML_EXEC, and PGSQL_DML_STRING");
RETURN_THROWS();
}

Expand Down
64 changes: 64 additions & 0 deletions ext/pgsql/tests/pg_dml_option_flags.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
--TEST--
pg_insert()/pg_update()/pg_delete(): the flags refused are the flags the message names
--EXTENSIONS--
pgsql
--SKIPIF--
<?php include("inc/skipif.inc"); ?>
--FILE--
<?php

include('inc/config.inc');
$table_name = 'table_pg_dml_option_flags';

$conn = pg_connect($conn_str);
pg_query($conn, "CREATE TABLE {$table_name} (id INT, id2 INT)");

/* PGSQL_DML_ASYNC is not part of the accepted mask of these two */
try {
pg_update($conn, $table_name, ['id2' => 2], ['id' => 1], PGSQL_DML_ASYNC | PGSQL_DML_STRING);
} catch (ValueError $e) {
echo $e->getMessage(), "\n";
}

try {
pg_delete($conn, $table_name, ['id' => 1], PGSQL_DML_ASYNC | PGSQL_DML_STRING);
} catch (ValueError $e) {
echo $e->getMessage(), "\n";
}

/* 1 << 13 is not one of the flags at all */
try {
pg_insert($conn, $table_name, ['id' => 1, 'id2' => 1], 1 << 13);
} catch (ValueError $e) {
echo $e->getMessage(), "\n";
}

/* but PGSQL_DML_ASYNC is accepted by pg_insert() and pg_select() */
var_dump(is_string(pg_insert($conn, $table_name, ['id' => 1, 'id2' => 1], PGSQL_DML_ASYNC | PGSQL_DML_STRING)));
var_dump(is_string(pg_select($conn, $table_name, ['id' => 1], PGSQL_DML_ASYNC | PGSQL_DML_STRING)));

/* every PGSQL_CONV_* flag the messages name is genuinely accepted */
var_dump(is_string(pg_insert($conn, $table_name, ['id' => 1, 'id2' => 1], PGSQL_CONV_IGNORE_DEFAULT | PGSQL_DML_STRING)));
var_dump(is_string(pg_update($conn, $table_name, ['id2' => 2], ['id' => 1], PGSQL_CONV_IGNORE_NOT_NULL | PGSQL_DML_STRING)));

/* PGSQL_DML_NO_CONV is accepted by pg_delete() and reaches its helper */
var_dump(is_string(pg_delete($conn, $table_name, ['id' => 1], PGSQL_DML_NO_CONV | PGSQL_DML_STRING)));

?>
--CLEAN--
<?php
include('inc/config.inc');
$table_name = 'table_pg_dml_option_flags';

$conn = pg_connect($conn_str);
pg_query($conn, "DROP TABLE IF EXISTS {$table_name}");
?>
--EXPECT--
pg_update(): Argument #5 ($flags) must be a valid bit mask of PGSQL_CONV_IGNORE_DEFAULT, PGSQL_CONV_FORCE_NULL, PGSQL_CONV_IGNORE_NOT_NULL, PGSQL_DML_NO_CONV, PGSQL_DML_ESCAPE, PGSQL_DML_EXEC, and PGSQL_DML_STRING
pg_delete(): Argument #4 ($flags) must be a valid bit mask of PGSQL_CONV_FORCE_NULL, PGSQL_DML_NO_CONV, PGSQL_DML_ESCAPE, PGSQL_DML_EXEC, and PGSQL_DML_STRING
pg_insert(): Argument #4 ($flags) must be a valid bit mask of PGSQL_CONV_IGNORE_DEFAULT, PGSQL_CONV_FORCE_NULL, PGSQL_CONV_IGNORE_NOT_NULL, PGSQL_DML_NO_CONV, PGSQL_DML_ESCAPE, PGSQL_DML_EXEC, PGSQL_DML_ASYNC, and PGSQL_DML_STRING
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
5 changes: 4 additions & 1 deletion ext/phar/phar.c
Original file line number Diff line number Diff line change
Expand Up @@ -1227,7 +1227,10 @@ static zend_result phar_parse_pharfile(php_stream *fp, const char *fname, size_t
} else {
str = entry.filename;
}
zend_hash_add_mem(&mydata->manifest, str, (void*)&entry, sizeof(phar_entry_info));
if (!zend_hash_add_mem(&mydata->manifest, str, (void*)&entry, sizeof(phar_entry_info))) {
phar_metadata_tracker_free(&entry.metadata_tracker, entry.is_persistent);
zend_string_free(entry.filename);
}
if (mydata->is_persistent) {
zend_string_release(str);
}
Expand Down
39 changes: 39 additions & 0 deletions ext/phar/tests/gh23477.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
--TEST--
GH-23477 (Memory leak on duplicate native Phar manifest entry)
--EXTENSIONS--
phar
--INI--
phar.require_hash=0
--FILE--
<?php
$stub = "<?php __HALT_COMPILER(); ?>\r\n";

function u32($value) {
return pack('V', $value);
}

function entry($name, $data, $metadata) {
$header = u32(strlen($name)) . $name
. u32(strlen($data)) . u32(0) . u32(strlen($data))
. u32(crc32($data)) . u32(0)
. u32(strlen($metadata)) . $metadata;
return [$header, $data];
}

$first = entry('a.txt', 'hello', 'i:1;');
$second = entry('a.txt', 'world', 'i:2;');
$manifest = u32(2) . "\x11\x00" . u32(0) . u32(0) . u32(0)
. $first[0] . $second[0];

file_put_contents(__DIR__ . '/gh23477.phar',
$stub . u32(strlen($manifest)) . $manifest . $first[1] . $second[1]);

$phar = new Phar(__DIR__ . '/gh23477.phar');
echo iterator_count($phar), "\n";
?>
--CLEAN--
<?php
@unlink(__DIR__ . '/gh23477.phar');
?>
--EXPECT--
1
7 changes: 5 additions & 2 deletions ext/standard/io_poll.c
Original file line number Diff line number Diff line change
Expand Up @@ -804,12 +804,15 @@ PHP_METHOD(Io_Poll_Context, wait)
if (max_events <= 0) {
max_events = 64;
}
} else if (max_events <= 0) {
} else if (UNEXPECTED(max_events <= 0)) {
zend_argument_value_error(2, "must be greater than 0");
RETURN_THROWS();
} else if (ZEND_LONG_INT_OVFL(max_events)) {
zend_argument_value_error(2, "must be less than or equal to %d", INT_MAX);
RETURN_THROWS();
}

php_poll_event *events = safe_emalloc(max_events, sizeof(*events), 0);
php_poll_event *events = safe_emalloc((size_t) max_events, sizeof(*events), 0);
int num_events = php_poll_wait(intern->ctx, events, (int) max_events, timeout ? &timeout_ts : NULL);

if (num_events < 0) {
Expand Down
27 changes: 27 additions & 0 deletions ext/standard/tests/filters/bucket_data_unset.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
--TEST--
unset(StreamBucket::$data) in filter callback must not crash when bucket is re-attached
--FILE--
<?php
class MyFilter extends php_user_filter {
public function filter($in, $out, &$consumed, bool $closing): int {
while ($bucket = stream_bucket_make_writeable($in)) {
unset($bucket->data);
stream_bucket_prepend($out, $bucket);
}
return PSFS_PASS_ON;
}
}
stream_filter_register("myfilter", "MyFilter");
$fp = fopen("php://temp", "w+");
fwrite($fp, str_repeat("A", 100));
rewind($fp);
stream_filter_append($fp, "myfilter");
try {
var_dump(stream_get_contents($fp));
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), "\n";
}
echo "DONE\n";
--EXPECT--
Error: Typed property StreamBucket::$data must not be accessed before initialization
DONE
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
--TEST--
stream_filter_remove() compacts unread data before appending flushed data
--FILE--
<?php
class ClosingSuffixFilter extends php_user_filter
{
public function filter($in, $out, &$consumed, $closing): int
{
while ($bucket = stream_bucket_make_writeable($in)) {
$consumed += $bucket->datalen;
stream_bucket_append($out, $bucket);
}
if ($closing) {
stream_bucket_append($out, stream_bucket_new($this->stream, 'END'));
}
return PSFS_PASS_ON;
}
}
stream_filter_register('closing-suffix', ClosingSuffixFilter::class);
$stream = fopen('php://memory', 'w+');
fwrite($stream, 'abcdef');
rewind($stream);
$filter = stream_filter_append($stream, 'closing-suffix', STREAM_FILTER_READ);
var_dump(fread($stream, 2));
var_dump(stream_filter_remove($filter));
var_dump(stream_get_contents($stream));
?>
--EXPECT--
string(2) "ab"
bool(true)
string(7) "cdefEND"
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,27 @@ try {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}

try {
$poll_ctx->wait(maxEvents: PHP_INT_MIN);
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}

try {
$poll_ctx->wait(maxEvents: -1);
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}

try {
$poll_ctx->wait(maxEvents: 0);
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}

?>
--EXPECT--
ValueError: Io\Poll\Context::wait(): Argument #1 ($timeout) must not be negative
ValueError: Io\Poll\Context::wait(): Argument #2 ($maxEvents) must be greater than 0
ValueError: Io\Poll\Context::wait(): Argument #2 ($maxEvents) must be greater than 0
ValueError: Io\Poll\Context::wait(): Argument #2 ($maxEvents) must be greater than 0
23 changes: 23 additions & 0 deletions ext/standard/tests/poll/poll_ctx_wait_error_int64.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
--TEST--
Io\Poll\Context::wait(): Parameter validation upper limit
--SKIPIF--
<?php
if (PHP_INT_SIZE <= 4) {
die("skip this test is for > 32bit platforms only");
}
?>
--FILE--
<?php
require_once __DIR__ . '/poll.inc';

$poll_ctx = new Io\Poll\Context();

try {
$poll_ctx->wait(maxEvents: PHP_INT_MAX);
} catch (Throwable $e) {
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
}

?>
--EXPECT--
ValueError: Io\Poll\Context::wait(): Argument #2 ($maxEvents) must be less than or equal to 2147483647
4 changes: 4 additions & 0 deletions ext/standard/user_filters.c
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,11 @@ static void php_stream_bucket_attach(int append, INTERNAL_FUNCTION_PARAMETERS)
}

if (NULL != (pzdata = zend_read_property(NULL, Z_OBJ_P(zobject), "data", sizeof("data")-1, false, &rv))) {
if (EG(exception)) {
RETURN_THROWS();
}
ZVAL_DEREF(pzdata);
ZEND_ASSERT(Z_TYPE_P(pzdata) == IS_STRING);
if (!bucket->own_buf) {
bucket = php_stream_bucket_make_writeable(bucket);
}
Expand Down
4 changes: 2 additions & 2 deletions main/streams/filter.c
Original file line number Diff line number Diff line change
Expand Up @@ -506,9 +506,9 @@ PHPAPI zend_result php_stream_filter_flush(php_stream_filter *filter, bool finis
/* Dump any newly flushed data to the read buffer */
if (stream->readpos > 0) {
/* Back the buffer up */
memcpy(stream->readbuf, stream->readbuf + stream->readpos, stream->writepos - stream->readpos);
stream->readpos = 0;
memmove(stream->readbuf, stream->readbuf + stream->readpos, stream->writepos - stream->readpos);
stream->writepos -= stream->readpos;
stream->readpos = 0;
}
if (flushed_size > (stream->readbuflen - stream->writepos)) {
/* Grow the buffer */
Expand Down
Loading