Skip to content

Fix pdo_odbc output-buffer leak and stale-binding out-of-bounds read#22735

Closed
iliaal wants to merge 1 commit into
php:PHP-8.4from
iliaal:fix/pdo-odbc-param-lifecycle
Closed

Fix pdo_odbc output-buffer leak and stale-binding out-of-bounds read#22735
iliaal wants to merge 1 commit into
php:PHP-8.4from
iliaal:fix/pdo-odbc-param-lifecycle

Conversation

@iliaal

@iliaal iliaal commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

pdo_odbc bound each parameter with SQLBindParameter at PDO_PARAM_EVT_ALLOC, recording deferred pointers (P->outbuf for output, &P->len, and the bound-parameter struct itself for input) into the ODBC statement, and never freed P->outbuf. Both outlive the bound-parameter struct, which PDO destroys mid-statement when execute() is given an array or a position is rebound, so the output buffer leaked and the stale binding made the next SQLExecute read freed memory. This tracks the output buffers on the statement and frees them in the destructor after SQLFreeHandle, and defers SQLBindParameter to execute time, rebinding the current parameters when the parameter set has changed.

Reproducers, both under a debug or ASAN build:

// out-of-bounds read (freed length indicator drives a wild over-read)
$stmt = $pdo->prepare('SELECT ? AS v');
$v = 'x';
$stmt->bindParam(1, $v, PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT, 256);
$stmt->execute([]);

// leak of the output buffer
$stmt = $pdo->prepare('SELECT ? AS v');
$stmt->bindParam(1, $v, PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT, 256);
$stmt->execute();

iliaal added a commit to iliaal/php-src that referenced this pull request Jul 14, 2026
SQLBindParameter recorded deferred pointers (P->outbuf, &P->len, and the
bound-parameter struct) into the ODBC statement at PDO_PARAM_EVT_ALLOC, and
P->outbuf was never freed. Both outlive the bound-parameter struct, which
PDO destroys mid-statement on execute() with an array or a rebind: the
buffer leaked and the stale binding made the next SQLExecute read freed
memory. Track output buffers on the statement and free them in the
destructor, and defer binding to execute time, resetting and rebinding the
current parameters when the set changed.

Closes phpGH-22735
@iliaal iliaal force-pushed the fix/pdo-odbc-param-lifecycle branch from fa0df82 to 5406151 Compare July 14, 2026 23:26

@NattyNarwhal NattyNarwhal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial pass, looks good other than a few nitpicky things related to clarity.

Comment thread ext/pdo_odbc/odbc_stmt.c Outdated
Comment thread ext/pdo_odbc/odbc_stmt.c Outdated
P->outbuflen = P->len;
if (!S->out_buffers) {
ALLOC_HASHTABLE(S->out_buffers);
zend_hash_init(S->out_buffers, 8, NULL, odbc_free_out_buffer, 0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If 8 is supposed to be pointer size, it should be using sizeof to make that clear

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's zend_hash_init's nSize (initial table size), not a pointer size, so sizeof doesn't apply.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My misread there. That said, I'm still not a huge fan of the magic number here, if it's a guess on the typical high end of param count, we should be clear here; a lot of instances of zend_hash_init are not. (Oh, and persistent param at end should also probably be false since it's a bool param, but that's not consistent through most of the codebase... opportunity for cleanup?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed the persistent flag to false and replaced the 8 with HT_MIN_SIZE to make the minimum-table intent explicit.

Comment thread ext/pdo_odbc/odbc_stmt.c Outdated
iliaal added a commit to iliaal/php-src that referenced this pull request Jul 15, 2026
SQLBindParameter recorded deferred pointers (P->outbuf, &P->len, and the
bound-parameter struct) into the ODBC statement at PDO_PARAM_EVT_ALLOC, and
P->outbuf was never freed. Both outlive the bound-parameter struct, which
PDO destroys mid-statement on execute() with an array or a rebind: the
buffer leaked and the stale binding made the next SQLExecute read freed
memory. Track output buffers on the statement and free them in the
destructor, and defer binding to execute time, resetting and rebinding the
current parameters when the set changed.

Closes phpGH-22735
@iliaal iliaal force-pushed the fix/pdo-odbc-param-lifecycle branch from 5406151 to f804cbe Compare July 15, 2026 16:28
SQLBindParameter recorded deferred pointers (P->outbuf, &P->len, and the
bound-parameter struct) into the ODBC statement at PDO_PARAM_EVT_ALLOC, and
P->outbuf was never freed. Both outlive the bound-parameter struct, which
PDO destroys mid-statement on execute() with an array or a rebind: the
buffer leaked and the stale binding made the next SQLExecute read freed
memory. Track output buffers on the statement and free them in the
destructor, and defer binding to execute time, resetting and rebinding the
current parameters when the set changed.

Closes phpGH-22735
@iliaal iliaal force-pushed the fix/pdo-odbc-param-lifecycle branch from f804cbe to 3a1a7d4 Compare July 15, 2026 19:03

@NattyNarwhal NattyNarwhal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems OK to me now

@iliaal iliaal closed this in bdcf9ce Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants