diff --git a/NEWS b/NEWS index d0510be26579..500f75cc050d 100644 --- a/NEWS +++ b/NEWS @@ -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 diff --git a/ext/pgsql/pgsql.c b/ext/pgsql/pgsql.c index 9f0ca2c1a5e6..791cbfd2c8fe 100644 --- a/ext/pgsql/pgsql.c +++ b/ext/pgsql/pgsql.c @@ -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(); } @@ -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(); } @@ -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; @@ -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(); } diff --git a/ext/pgsql/tests/pg_dml_option_flags.phpt b/ext/pgsql/tests/pg_dml_option_flags.phpt new file mode 100644 index 000000000000..3f4b73671aa1 --- /dev/null +++ b/ext/pgsql/tests/pg_dml_option_flags.phpt @@ -0,0 +1,64 @@ +--TEST-- +pg_insert()/pg_update()/pg_delete(): the flags refused are the flags the message names +--EXTENSIONS-- +pgsql +--SKIPIF-- + +--FILE-- + 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-- + +--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) diff --git a/ext/phar/phar.c b/ext/phar/phar.c index af3a4992d6d5..f64f27bac383 100644 --- a/ext/phar/phar.c +++ b/ext/phar/phar.c @@ -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); } diff --git a/ext/phar/tests/gh23477.phpt b/ext/phar/tests/gh23477.phpt new file mode 100644 index 000000000000..cd015ddaa9b0 --- /dev/null +++ b/ext/phar/tests/gh23477.phpt @@ -0,0 +1,39 @@ +--TEST-- +GH-23477 (Memory leak on duplicate native Phar manifest entry) +--EXTENSIONS-- +phar +--INI-- +phar.require_hash=0 +--FILE-- +\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-- + +--EXPECT-- +1 diff --git a/ext/standard/io_poll.c b/ext/standard/io_poll.c index f57813874d52..a8a0563627fa 100644 --- a/ext/standard/io_poll.c +++ b/ext/standard/io_poll.c @@ -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) { diff --git a/ext/standard/tests/filters/bucket_data_unset.phpt b/ext/standard/tests/filters/bucket_data_unset.phpt new file mode 100644 index 000000000000..043ea5953e92 --- /dev/null +++ b/ext/standard/tests/filters/bucket_data_unset.phpt @@ -0,0 +1,27 @@ +--TEST-- +unset(StreamBucket::$data) in filter callback must not crash when bucket is re-attached +--FILE-- +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 diff --git a/ext/standard/tests/filters/stream_filter_remove_compact_read_buffer.phpt b/ext/standard/tests/filters/stream_filter_remove_compact_read_buffer.phpt new file mode 100644 index 000000000000..7cfef971e7e7 --- /dev/null +++ b/ext/standard/tests/filters/stream_filter_remove_compact_read_buffer.phpt @@ -0,0 +1,31 @@ +--TEST-- +stream_filter_remove() compacts unread data before appending flushed data +--FILE-- +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" diff --git a/ext/standard/tests/poll/poll_ctx_wait.phpt b/ext/standard/tests/poll/poll_ctx_wait_error.phpt similarity index 58% rename from ext/standard/tests/poll/poll_ctx_wait.phpt rename to ext/standard/tests/poll/poll_ctx_wait_error.phpt index 5080c1421fdb..c514884c3c6f 100644 --- a/ext/standard/tests/poll/poll_ctx_wait.phpt +++ b/ext/standard/tests/poll/poll_ctx_wait_error.phpt @@ -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 diff --git a/ext/standard/tests/poll/poll_ctx_wait_error_int64.phpt b/ext/standard/tests/poll/poll_ctx_wait_error_int64.phpt new file mode 100644 index 000000000000..3d881050e655 --- /dev/null +++ b/ext/standard/tests/poll/poll_ctx_wait_error_int64.phpt @@ -0,0 +1,23 @@ +--TEST-- +Io\Poll\Context::wait(): Parameter validation upper limit +--SKIPIF-- + 32bit platforms only"); +} +?> +--FILE-- +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 diff --git a/ext/standard/user_filters.c b/ext/standard/user_filters.c index e65ddd78ba4d..b699c12ed9ba 100644 --- a/ext/standard/user_filters.c +++ b/ext/standard/user_filters.c @@ -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); } diff --git a/main/streams/filter.c b/main/streams/filter.c index 1be5cb1b3e50..fdc5dfc00ad2 100644 --- a/main/streams/filter.c +++ b/main/streams/filter.c @@ -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 */