Skip to content

pdo_pgsql: out-of-bounds read when a re-executed statement returns more columns #25184

Open

Description

@dixyes

Description

pdo_pgsql indexes S->cols and stmt->columns with the column count of the current result, but both arrays are still sized for the first result of that statement. When a prepared statement is executed a second time and the second result has more columns, the reads run past the end of both arrays. The stmt->columns side is the dangerous one: pdo_stmt.c uses the new stmt->column_count as the bound and then hands stmt->columns[colno].name to zend_symtable_update() as a zend_string *, so a stale pointer is dereferenced.

This needs PDO::ATTR_EMULATE_PREPARES to be enabled. With a native prepared statement the server rejects the changed result shape first with cached plan must not change result type.

Triggering it is routine: SELECT * followed by a concurrent ALTER TABLE ... ADD COLUMN, a view swap, or a changed search_path between two executions. A malicious or compromised server can also extend the result of the second execute.

--TEST--
PDO PgSQL: out-of-bounds read when a re-executed statement returns more columns
--EXTENSIONS--
pdo_pgsql
--SKIPIF--
<?php
require __DIR__ . '/config.inc';
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
PDOTest::skip();
?>
--FILE--
<?php

require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';

$pdo = PDOTest::test_factory(__DIR__ . '/common.phpt');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// the query has to be sent again on every execute, otherwise the server rejects
// the changed result type of a cached plan
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);

$pdo->exec('CREATE TEMPORARY TABLE pdo_pgsql_repro (a integer)');
$pdo->exec('INSERT INTO pdo_pgsql_repro VALUES (1)');

$stmt = $pdo->prepare('SELECT *, ? AS extra FROM pdo_pgsql_repro');
$stmt->execute([7]);
var_dump($stmt->columnCount());
var_dump($stmt->fetch(PDO::FETCH_NUM));

// the second result has one more column than the arrays that describe it
$pdo->exec('ALTER TABLE pdo_pgsql_repro ADD COLUMN b text');
$pdo->exec("UPDATE pdo_pgsql_repro SET b = 'x'");

$stmt->execute([7]);
var_dump($stmt->columnCount());
var_dump($stmt->fetch(PDO::FETCH_NUM));

echo "Done\n";

?>
--EXPECT--
int(2)
array(2) {
  [0]=>
  string(1) "1"
  [1]=>
  string(1) "7"
}
int(3)
array(3) {
  [0]=>
  string(1) "1"
  [1]=>
  string(1) "x"
  [2]=>
  string(1) "7"
}
Done

Observed on an ASan build of an unpatched tree:

==3==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x...
READ of size 4
    #0 pgsql_stmt_get_col ext/pdo_pgsql/pgsql_statement.c:751
    #1 fetch_value ext/pdo/pdo_stmt.c:512
    #2 do_fetch ext/pdo/pdo_stmt.c:861
    #3 zim_PDOStatement_fetch ext/pdo/pdo_stmt.c:1066
0x... is located 0 bytes after 8-byte region
allocated by thread T0 here:
    #3 pgsql_stmt_execute ext/pdo_pgsql/pgsql_statement.c:458
SUMMARY: AddressSanitizer: heap-buffer-overflow

Fix for reference:

diff --git a/ext/pdo_pgsql/pgsql_statement.c b/ext/pdo_pgsql/pgsql_statement.c
index e2c8a35944c..8f627b2825f 100644
--- a/ext/pdo_pgsql/pgsql_statement.c
+++ b/ext/pdo_pgsql/pgsql_statement.c
@@ -453,9 +453,19 @@ static int pgsql_stmt_execute(pdo_stmt_t *stmt)
 		return 0;
 	}
 
-	stmt->column_count = (int) PQnfields(S->result);
-	if (S->cols == NULL) {
-		S->cols = ecalloc(stmt->column_count, sizeof(pdo_pgsql_column));
+	/* A re-executed statement may return more columns than the first time, so
+	 * both descriptions are resized together: S->cols here, and stmt->columns
+	 * by letting PDO describe the columns again on the next fetch */
+	int column_count = (int) PQnfields(S->result);
+	if (S->cols == NULL || stmt->column_count != column_count) {
+		if (S->cols != NULL) {
+			efree(S->cols);
+		}
+		S->cols = column_count > 0
+			? ecalloc(column_count, sizeof(pdo_pgsql_column))
+			: NULL;
+		php_pdo_stmt_set_column_count(stmt, column_count);
+		stmt->executed = 0;
 	}
 
 	if (status == PGRES_COMMAND_OK) {

PHP Version

master

Operating System

No response

Activity

  1. devnexen commented on Nov 4, 2026

    @devnexen
    Member

    Hi @dixyes feel free to open a PR. Cheers.

  2. dixyes commented on Nov 4, 2026

    @dixyes
    ContributorAuthor

    2. Hi @dixyes feel free to open a PR. Cheers.

    I will no longer be submitting PRs to PHP. I don’t have the time or energy to maintain them and push for their merger. However, my code is still shared with waived copyright/attribution rights—feel free to use it however you like.

  3. dixyes commented on Nov 4, 2026

    @dixyes
    Author
  4. self-assigned this
    on Nov 4, 2026
  5. HackingRepo commented on Nov 4, 2026

    @HackingRepo

    this probably a security issue, why did you posted publicly @dixyes, very weird to me, this should bd a security advisory?

    what it do not meet

    you said already a malicious server this is clearly a security issue, this is not the place to do so, can you confirm this, thank's, and correct me

  6. dixyes commented on Nov 5, 2026

    @dixyes
    ContributorAuthor

    8. this probably a security issue, why did you posted publicly @dixyes, very weird to me, this should bd a security advisory?
    what it do not meet
    you said already a malicious server this is clearly a security issue, this is not the place to do so, can you confirm this, thank's, and correct me

    Does a user actively connecting to a malicious PostgreSQL server constitute a security risk? Can this bug be exploited remotely (or even locally)? I can't imagine that...

  7. HackingRepo commented on Nov 5, 2026

    @HackingRepo

    yes some hosting provider written in php say enter your postgresql server and then entering the malicious server will take over the hosting provider and can achieve RCE @dixyes, think consider these scenarios

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions