Repository navigation
A crash occurred in the odbc_handle_closer function #20726
Copy link
Copy link
Open
Description
There is an issue with the order of resource release that needs to be corrected by modifying the code.
php-src/ext/pdo_odbc/odbc_driver.c
Line 138 in 833120e
| SQLFreeHandle(SQL_HANDLE_ENV, H->env); |
The correct code might be:
static void odbc_handle_closer(pdo_dbh_t *dbh) {
pdo_odbc_db_handle *H = (pdo_odbc_db_handle *) dbh->driver_data;
SQLFreeHandle(SQL_HANDLE_ENV, H->env);
H->env = NULL;
if (H->dbc != SQL_NULL_HANDLE) {
SQLEndTran(SQL_HANDLE_DBC, H->dbc, SQL_ROLLBACK);
SQLDisconnect(H->dbc);
SQLFreeHandle(SQL_HANDLE_DBC, H->dbc);
H->dbc = NULL;
}
pefree(H, dbh->is_persistent);
dbh->driver_data = NULL;
}I don't quite understand the logic of the ODBC PDO driver, please review the code above.
==45879== Invalid read of size 8
==45879== at 0x1185F941: ??? (in /usr/lib/libodbccr.so.2.0.0)
==45879== by 0x734F411: ??? (in /usr/lib/libodbc.so.2.0.0)
==45879== by 0x7351182: ??? (in /usr/lib/libodbc.so.2.0.0)
==45879== by 0x7359691: ??? (in /usr/lib/libodbc.so.2.0.0)
==45879== by 0x7B01E10: odbc_handle_closer (odbc_driver.c:139)
==45879== by 0x73F800E: ??? (in /usr/lib/php84/modules/pdo.so)
==45879== by 0x5CE98E: zend_objects_store_del (in /usr/bin/php84)
==45879== by 0x586F32: ??? (in /usr/bin/php84)
==45879== by 0x534247: execute_ex (in /usr/bin/php84)
==45879== by 0x52D64B: zend_call_function (in /usr/bin/php84)
==45879== by 0x799996A: swoole::PHPCoroutine::main_func(void*) (swoole_coroutine.cc:773)
==45879== by 0x7A2B6C7: operator() (std_function.h:591)
==45879== by 0x7A2B6C7: swoole::coroutine::Context::context_func(transfer_t) (context.cc:149)
==45879== Address 0x11079928 is 2,344 bytes inside a block of size 5,256 free'd
==45879== at 0x48B2ECF: free (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==45879== by 0x73788CA: ??? (in /usr/lib/libodbc.so.2.0.0)
==45879== by 0x73597E7: ??? (in /usr/lib/libodbc.so.2.0.0)
==45879== by 0x7B01DFB: odbc_handle_closer (odbc_driver.c:136)
==45879== by 0x73F800E: ??? (in /usr/lib/php84/modules/pdo.so)
==45879== by 0x5CE98E: zend_objects_store_del (in /usr/bin/php84)
==45879== by 0x586F32: ??? (in /usr/bin/php84)
==45879== by 0x534247: execute_ex (in /usr/bin/php84)
==45879== by 0x52D64B: zend_call_function (in /usr/bin/php84)
==45879== by 0x799996A: swoole::PHPCoroutine::main_func(void*) (swoole_coroutine.cc:773)
==45879== by 0x7A2B6C7: operator() (std_function.h:591)
==45879== by 0x7A2B6C7: swoole::coroutine::Context::context_func(transfer_t) (context.cc:149)
==45879== by 0x7B1F70E: swoole_make_fcontext (make_x86_64_sysv_elf_gas.S:135)
==45879== Block was alloc'd at
==45879== at 0x48B640A: calloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==45879== by 0x7378711: ??? (in /usr/lib/libodbc.so.2.0.0)
==45879== by 0x734A3EE: ??? (in /usr/lib/libodbc.so.2.0.0)
==45879== by 0x7B022FE: pdo_odbc_handle_factory (odbc_driver.c:498)
==45879== by 0x73F2A2D: php_pdo_internal_construct_driver (in /usr/lib/php84/modules/pdo.so)
==45879== by 0x552BD0: ??? (in /usr/bin/php84)
==45879== by 0x534247: execute_ex (in /usr/bin/php84)
==45879== by 0x52D64B: zend_call_function (in /usr/bin/php84)
==45879== by 0x799996A: swoole::PHPCoroutine::main_func(void*) (swoole_coroutine.cc:773)
==45879== by 0x7A2B6C7: operator() (std_function.h:591)
==45879== by 0x7A2B6C7: swoole::coroutine::Context::context_func(transfer_t) (context.cc:149)
==45879== by 0x7B1F70E: swoole_make_fcontext (make_x86_64_sysv_elf_gas.S:135)
Activity
ndossche commentedon Dec 18, 2025
The handle is used when creating the DBC handle:
php-src/ext/pdo_odbc/odbc_driver.c
Lines 507 to 511 in 06ea0e5
So I think that the env must be destroyed after destroying the DBC. But that's what the current code already does.
Without a reproducer this is gonna be difficult.
cc @NattyNarwhal
NattyNarwhal commentedon Dec 18, 2025
Yeah, the environment handle should always be freed last, since the DBC is derived from the ENV. (The way the handles are structured makes me think it might be possible to just make one in module state and share it across multiple connections, but that's something else.)
I'm guessing you were able to reproduce it on your own, since the linked issue's reproducer seems simple. Does it reproduce outside of Swoole?
NattyNarwhal commentedon Dec 18, 2025
Can reproduce on master without Swoole (on ppc64, since that's the system I had on hand, and running Gentoo, so unixODBC symbols are easy):
Code is (random mdb file from https://github.com/mdbtools/mdbtestdata/blob/master/data/nwind.mdb to placate it):
(edit: unixODBC trace shows the crash in the freeing of the environment handle)
NattyNarwhal commentedon Dec 18, 2025
What version of unixODBC are you using? The crash is occurring somewhere with the cursor library, and I noticed 2.3.13/2.3.14 seem to fix some bugs related to (de)init and the cursor library. Indeed, switching my system from 2.3.12 to 2.3.14 seems to allegedly shift the crash somewhere else instead:
I'm not entirely sure if this is more of an MDBTools/unixODBC bug and PHP is just the messenger being shot here too.
NattyNarwhal commentedon Dec 18, 2025
I think this is an interaction between mdbtools and unixODBC's connection pooling:
I also can't reproduce this with another ODBC driver (i.e. MySQL's).
matyhtf commentedon Dec 19, 2025
Version:
NattyNarwhal commentedon Dec 19, 2025
What happens if you turn off connection pooling (either via the PHP INI option, or via the unixODBC odbcinst.ini option CPTimeout = 0 on the driver)?
I really suspect this is some interaction with MDBTools and unixODBC, based on the connection pool behaviour and lack of problems with other drivers. I'm trying to reproduce this outside of PHP to test that, but haven't had much luck.
1 remaining item
matyhtf commentedon Dec 23, 2025
After disabling the connection pool, there was no crash.
Fix phpGH-20726: crash in pdo_odbc with pooling and no credentials
iliaal commentedon Jun 29, 2026
Reproduced on current master (built pdo_odbc against unixODBC 2.3.9 with the MDBTools driver). With connection pooling active the connect/teardown crashes; with pooling off it's clean, matching your observation.
The reorder in your snippet isn't the fix though. master already frees the connection handle before the environment handle:
That's the correct ODBC order: the environment is the parent of the connection, so it has to be freed last. Freeing
SQL_HANDLE_ENVfirst, as the snippet does, inverts the handle hierarchy.The crash is inside the driver manager's pooling, not PHP's cleanup. pdo_odbc allocates a fresh HENV per connection and frees it on close, while pooling is a process-global attribute; unixODBC's pool bookkeeping outlives the per-connection HENV and dereferences stale or NULL state. In my repro the fault is a NULL strcmp inside libodbc during SQLConnect pool-matching, triggered because pdo_odbc passes NULL username/password straight through. Passing empty strings removes the hard crash (#22512); what's left is an uninitialised-value read inside libodbc's pooling, which is a unixODBC bug.
Worth reporting the remaining pooling defect to unixODBC.
Fix phpGH-20726: crash in pdo_odbc with pooling and no credentials
Fix GH-20726: crash in pdo_odbc with pooling and no credentials (#22512)
Merge branch 'PHP-8.5'
Merge branch 'PHP-8.4' into PHP-8.5
iliaal commentedon Jun 30, 2026
Reopening. The merged patch only guards the NULL-credential connect path against the pooling
strcmp()crash. The originalodbc_handle_closeruse-after-free under pooling is a residual unixODBC defect and is still unfixed. The auto-close came from the squash-merge subject (Fix GH-20726), not from intent; the PR referenced it asRefsdeliberately.