From 91ea928c9b475a0fb653523ca99849883cca6d96 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Tue, 16 Jun 2026 16:23:28 -0400 Subject: [PATCH 1/2] Fix heap over-read seeding the long-column buffer in pdo_odbc In the long-column fetch path, when the ODBC driver reports the total column length rather than SQL_NO_TOTAL, the result string was seeded by copying orig_fetched_len + 1 bytes out of C->data, which holds at most LONG_COLUMN_BUFFER_SIZE bytes from the first SQLGetData. For a column larger than that buffer this reads past C->data. Seed only the bytes actually present in the buffer, matching the SQL_NO_TOTAL branch; the remainder is still fetched by the loop. Closes GH-22349 --- ext/pdo_odbc/odbc_stmt.c | 5 ++-- ext/pdo_odbc/tests/gh22349.phpt | 45 +++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 ext/pdo_odbc/tests/gh22349.phpt diff --git a/ext/pdo_odbc/odbc_stmt.c b/ext/pdo_odbc/odbc_stmt.c index 940cf1209b14..bc87bdb14560 100644 --- a/ext/pdo_odbc/odbc_stmt.c +++ b/ext/pdo_odbc/odbc_stmt.c @@ -704,8 +704,9 @@ static int odbc_stmt_get_col(pdo_stmt_t *stmt, int colno, zval *result, enum pdo } ssize_t to_fetch_byte = to_fetch_len + 1; char *buf2 = emalloc(to_fetch_byte); - zend_string *str = zend_string_init(C->data, to_fetch_byte, 0); - size_t used = to_fetch_len; + ssize_t seed_len = to_fetch_len > (LONG_COLUMN_BUFFER_SIZE - 1) ? (LONG_COLUMN_BUFFER_SIZE - 1) : to_fetch_len; + zend_string *str = zend_string_init(C->data, seed_len + 1, 0); + size_t used = seed_len; do { C->fetched_len = 0; diff --git a/ext/pdo_odbc/tests/gh22349.phpt b/ext/pdo_odbc/tests/gh22349.phpt new file mode 100644 index 000000000000..58219dea0ac6 --- /dev/null +++ b/ext/pdo_odbc/tests/gh22349.phpt @@ -0,0 +1,45 @@ +--TEST-- +GH-22349 (Heap over-read fetching a long column past the internal buffer) +--EXTENSIONS-- +pdo_odbc +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); + +$db->exec('DROP TABLE test_gh22349'); +if (false === $db->exec('CREATE TABLE test_gh22349 (data text)') + && false === $db->exec('CREATE TABLE test_gh22349 (data CLOB)') + && false === $db->exec('CREATE TABLE test_gh22349 (data longtext)')) { + die("BORK: no large text column type available here: " . implode(", ", $db->errorInfo()) . "\n"); +} + +$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +// The driver fetches a long column into an internal buffer of roughly one +// memory page and reassembles the remainder. Exercise values that span and +// exceed that buffer so the seeded length must match the bytes present. +foreach ([4096, 8192, 65536] as $len) { + $db->exec('DELETE FROM test_gh22349'); + $text = str_repeat('A', $len); + $db->exec("INSERT INTO test_gh22349 VALUES ('$text')"); + $got = $db->query('SELECT data FROM test_gh22349')->fetchColumn(); + printf("%d: %s\n", $len, ($got === $text) ? 'ok' : ('MISMATCH len=' . strlen($got))); +} +?> +--CLEAN-- +exec('DROP TABLE test_gh22349'); +?> +--EXPECT-- +4096: ok +8192: ok +65536: ok From 100938b40b0346e0f7ded39ccaf11440f961d8fb Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sun, 14 Jun 2026 12:06:23 -0400 Subject: [PATCH 2/2] Fix file descriptor leak when proc_open() descriptor setup fails When a descriptor spec entry fails to set up (unknown type, missing mode) after an earlier entry already opened a pipe or socket, proc_open() jumped to exit_fail without closing the descriptors it had already opened, leaking those fds; repeated calls exhaust the process descriptor table. Close the opened descriptors at exit_fail and drop the now-redundant per-call close before each spawn-failure goto. Closes GH-22311 --- ext/standard/proc_open.c | 7 +++---- .../proc_open_fd_leak_on_setup_failure.phpt | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 ext/standard/tests/general_functions/proc_open_fd_leak_on_setup_failure.phpt diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c index 278f7486e1ad..d2d51de5a856 100644 --- a/ext/standard/proc_open.c +++ b/ext/standard/proc_open.c @@ -1371,7 +1371,6 @@ PHP_FUNCTION(proc_open) if (newprocok == FALSE) { DWORD dw = GetLastError(); - close_all_descriptors(descriptors, ndesc); char *msg = php_win32_error_to_msg(dw); php_error_docref(NULL, E_WARNING, "CreateProcess failed: %s", msg); php_win32_error_msg_free(msg); @@ -1388,7 +1387,6 @@ PHP_FUNCTION(proc_open) if (close_parentends_of_pipes(&factions, descriptors, ndesc) == FAILURE) { posix_spawn_file_actions_destroy(&factions); - close_all_descriptors(descriptors, ndesc); goto exit_fail; } @@ -1408,7 +1406,6 @@ PHP_FUNCTION(proc_open) } posix_spawn_file_actions_destroy(&factions); if (r != 0) { - close_all_descriptors(descriptors, ndesc); php_error_docref(NULL, E_WARNING, "posix_spawn() failed: %s", strerror(r)); goto exit_fail; } @@ -1450,7 +1447,6 @@ PHP_FUNCTION(proc_open) _exit(127); } else if (child < 0) { /* Failed to fork() */ - close_all_descriptors(descriptors, ndesc); php_error_docref(NULL, E_WARNING, "Fork failed: %s", strerror(errno)); goto exit_fail; } @@ -1540,6 +1536,9 @@ PHP_FUNCTION(proc_open) } else { exit_fail: _php_free_envp(env); + if (descriptors) { + close_all_descriptors(descriptors, ndesc); + } RETVAL_FALSE; } diff --git a/ext/standard/tests/general_functions/proc_open_fd_leak_on_setup_failure.phpt b/ext/standard/tests/general_functions/proc_open_fd_leak_on_setup_failure.phpt new file mode 100644 index 000000000000..e072f75a82d2 --- /dev/null +++ b/ext/standard/tests/general_functions/proc_open_fd_leak_on_setup_failure.phpt @@ -0,0 +1,20 @@ +--TEST-- +proc_open() does not leak file descriptors when descriptor setup fails mid-spec +--SKIPIF-- + +--FILE-- + ["pipe", "r"], 1 => ["bogus_type"]], $pipes); +} +$after = count(scandir("/proc/self/fd")); +var_dump($after <= $before + 2); +?> +--EXPECT-- +bool(true)