php-src Adds Chunked Pgsql Fetch And Memory Guards


php-src is the C implementation of the PHP language. Recent master activity put 50 commits on the board, 280 files, 5591 insertions, and 707 deletions, with NEWS and UPGRADING taking most of the file churn as PHP 8.6 notes fill in. The operator visible work is a real fetch API for large Postgres result sets, plus crash and leak fixes in streams, Phar, FPM, and the allocator.

ext/pdo_pgsql now exposes the chunk size ext/pgsql already had as pg_set_chunked_rows_size(). The ATTR_CHUNK_SIZE change adds Pdo\Pgsql::ATTR_CHUNK_SIZE so a statement can pull N rows per round trip instead of buffering the whole result or walking one row at a time with PDO::ATTR_PREFETCH => 0. It needs libpq 17 or later.

Setting one of ATTR_PREFETCH and ATTR_CHUNK_SIZE on the connection clears the other. A statement that sets either does not inherit the connection value. A chunk size of 1 or more wins when both are set on the same statement. Combining a chunk size with a statement level PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL throws a ValueError. A connection level chunk size is disabled on a scrollable statement.

$stmt = $pdo->prepare(
    "SELECT * FROM events",
    [Pdo\Pgsql::ATTR_CHUNK_SIZE => 1000]
);
$stmt->execute();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    // rowCount() reports the current chunk size, not the full result
}

A sibling fix for scrollable cursors landed in pgsql_driver.c. PDO::CURSOR_SCROLL plus lazy fetch (ATTR_PREFETCH => 0) failed at execute() with SQLSTATE[HY000]: General error: 7 and no message. Either option alone worked. A cursor does not stream its result, but S->is_unbuffered stayed set, so execute called PQgetResult() with nothing in flight. The flag is now cleared when the statement has a cursor.

ext/pgsql also stopped listing PGSQL_DML_ASYNC among valid flags for pg_update() and pg_delete(), where the mask already refuses it.

Two stream bugs sat next to each other. A crash fix covers a user filter callback that unsets StreamBucket::$data before attaching the bucket again. stream_bucket_prepend() and stream_bucket_append() treated a successful zend_read_property() as a string. For an unset typed property that call throws and returns EG(uninitialized_zval), so Z_STRLEN_P() dereferenced a NULL pointer when the brigade was consumed. Reads that are not strings are now rejected and the pending exception is rethrown.

Flush compaction in php_stream_filter_flush() used memcpy() on overlapping unread ranges, which is undefined, and reset readpos before subtracting it from writepos, leaving stale data in the buffer. It now uses memmove() and matches php_stream_fill_read_buffer().

HTTP wrappers had an out of bounds read on an empty Location header. An empty header allocates one byte for the NUL terminator, so location[1] in the relative redirect branch read past the allocation and could append a garbage path. The code now uses location_len and skips the relative join when the length is 0.

The CLI dev server also answers Expect: 100-continue on HTTP/1.1. curl waits for 100 Continue before sending a large POST body, and the missing reply added a one second timeout. HTTP/1.0 still skips the reply.

Packaged PHP jobs and FPM pools picked up real faults. Use after free on mounted Phar subdirectories (GH-23418) passed a shortened path into phar_mount_entry() without keeping the source alive through error formatting and manifest lookup. Duplicate native manifest entries leaked because insert failure did not free the new allocations.

FPM rejected out of range numeric UIDs and GIDs (GH-19320). Pool user/group and listen.owner/listen.group used strtoul() with no range check, so values past uid_t/gid_t overflowed. Parse now goes through strtoumax(), compares against the type max, and refuses the (uid_t)-1 sentinel.

zend_alloc.c detects consecutive double frees of small slots. Freeing the same small pointer twice pushed it onto the freelist twice, so the next two allocations of that bin returned the same address. The check is a compare against heap->free_slot[bin_num] and only catches the immediate case. It will not see a free after other activity on the same bin.

SoapServer no longer segfaults when setClass() points at a class that fails object_init_ex(), for example a property default that references an undefined constant. Failure is now a SOAP fault, the same path a throwing constructor already takes.

URL builders and Linux reuseport filters both got stricter contracts. The URI followup work lands Uri\WhatWg\url_percent_encode() plus Uri\WhatWg\UrlPercentEncodingMode. Modes cover username, password, opaque host, path, opaque path, path segment, query, special query, form query, and fragment. Form query maps space to +. The wiring sits in php_uri.c.

Sockets tightened SO_ATTACH_REUSEPORT_CBPF in sockets.c. The value must be int and the level must be SOL_SOCKET. Other types throw TypeError instead of being coerced. A zero value used to call SO_DETACH_BPF, which left the reuseport program attached. Detach now uses SO_DETACH_REUSEPORT_BPF on Linux and exposes that constant. Platforms without the option warn and return false.

SplFileObject CSV methods are deprecated since 8.6: fgetcsv(), fputcsv(), setCsvControl(), and getCsvControl(). The deprecation is the 8.6 RFC. ETL scripts that parse CSV through SplFileObject need a move to fgetcsv() and fputcsv() on a plain file handle.

unpack() now treats < and > right after a format code as endianness modifiers. The upgrade note is the part operators will hit. "s<value" produces the key "value" instead of "<value". "C>name" raises ValueError because C takes no endianness modifier. A repeater still protects a name, as in "s1<value".

NEWS already has an 8.6.0beta3 heading. Chunk size needs libpq 17. Reuseport detach is Linux only. If any worker still combines PDO::CURSOR_SCROLL with ATTR_PREFETCH => 0, retest execute() on that path.