Skip to content

A crash occurred in the odbc_handle_closer function #20726

Open

Description

@matyhtf

There is an issue with the order of resource release that needs to be corrected by modifying the code.

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

  1. ndossche commented on Dec 18, 2025

    @ndossche
    Member

    The handle is used when creating the DBC handle:

    rc = SQLAllocHandle(SQL_HANDLE_DBC, H->env, &H->dbc);
    if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
    pdo_odbc_drv_error("SQLAllocHandle: DBC");
    goto fail;
    }

    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

  2. NattyNarwhal commented on Dec 18, 2025

    @NattyNarwhal
    Member

    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?

  3. NattyNarwhal commented on Dec 18, 2025

    @NattyNarwhal
    Member

    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):

    (gdb) run
    Starting program: /home/calvin/php-src/sapi/cli/php ../mdbtools.php
    [Thread debugging using libthread_db enabled]
    Using host libthread_db library "/usr/lib64/libthread_db.so.1".
    
    Program received signal SIGSEGV, Segmentation fault.
    0x00003ffff6da238c in CLDisconnect (connection_handle=0x102272b00) at /usr/src/debug/dev-db/unixODBC-2.3.12-r2/unixODBC-2.3.12/cur/SQLConnect.c:428
    428	            connection -> functions[ i ] =
    (gdb) bt
    #0  0x00003ffff6da238c in CLDisconnect (connection_handle=0x102272b00) at /usr/src/debug/dev-db/unixODBC-2.3.12-r2/unixODBC-2.3.12/cur/SQLConnect.c:428
    #1  0x00003ffff79dd1f4 in close_pooled_connection (ptr=ptr@entry=0x10228a160)
        at /usr/src/debug/dev-db/unixODBC-2.3.12-r2/unixODBC-2.3.12/DriverManager/SQLConnect.c:2916
    #2  0x00003ffff79dfb40 in __strip_from_pool (env=env@entry=0x10225c710)
        at /usr/src/debug/dev-db/unixODBC-2.3.12-r2/unixODBC-2.3.12/DriverManager/SQLConnect.c:3101
    #3  0x00003ffff79eba38 in __SQLFreeHandle (handle_type=<optimized out>, handle=0x10225c710)
        at /usr/src/debug/dev-db/unixODBC-2.3.12-r2/unixODBC-2.3.12/DriverManager/SQLFreeHandle.c:283
    #4  0x00003ffff79ec010 in SQLFreeHandle (handle_type=<optimized out>, handle=<optimized out>)
        at /usr/src/debug/dev-db/unixODBC-2.3.12-r2/unixODBC-2.3.12/DriverManager/SQLFreeHandle.c:662
    #5  0x00000001005e0c74 in odbc_handle_closer (dbh=0x3ffff745c240) at /home/calvin/php-src/ext/pdo_odbc/odbc_driver.c:138
    #6  0x00000001005ce234 in dbh_free (dbh=0x3ffff745c240, free_persistent=false) at /home/calvin/php-src/ext/pdo/pdo_dbh.c:1562
    #7  0x00000001005ce6a0 in pdo_dbh_free_storage (std=0x3ffff74951e8) at /home/calvin/php-src/ext/pdo/pdo_dbh.c:1611
    #8  0x0000000100bd9784 in zend_objects_store_del (object=0x3ffff74951e8) at /home/calvin/php-src/Zend/zend_objects_API.c:196
    #9  0x0000000100c06bdc in rc_dtor_func (p=0x3ffff74951e8) at /home/calvin/php-src/Zend/zend_variables.c:57
    #10 0x0000000100c06afc in i_zval_ptr_dtor (zval_ptr=0x3fffffffb098) at /home/calvin/php-src/Zend/zend_variables.h:45
    #11 0x0000000100c06e5c in zval_ptr_dtor (zval_ptr=0x3fffffffb098) at /home/calvin/php-src/Zend/zend_variables.c:84
    #12 0x0000000100b4620c in _zend_hash_del_el_ex (ht=0x101fb4c00, idx=9, p=0x3ffff745d320, prev=0x0) at /home/calvin/php-src/Zend/zend_hash.c:1500
    #13 0x0000000100b46370 in _zend_hash_del_el (ht=0x101fb4c00, idx=9, p=0x3ffff745d320) at /home/calvin/php-src/Zend/zend_hash.c:1527
    #14 0x0000000100b491f8 in zend_hash_reverse_apply (ht=0x101fb4c00, apply_func=@0x101e66d08: 0x100a4ceb8 <zval_call_destructor>)
        at /home/calvin/php-src/Zend/zend_hash.c:2243
    #15 0x0000000100a4d230 in shutdown_destructors () at /home/calvin/php-src/Zend/zend_execute_API.c:262
    #16 0x0000000100c16588 in zend_call_destructors () at /home/calvin/php-src/Zend/zend.c:1342
    #17 0x00000001008e9234 in php_request_shutdown (dummy=0x0) at /home/calvin/php-src/main/main.c:1985
    #18 0x0000000100c1e028 in do_cli (argc=2, argv=0x101fb1540) at /home/calvin/php-src/sapi/cli/php_cli.c:1158
    #19 0x0000000100c1e6d4 in main (argc=2, argv=0x101fb1540) at /home/calvin/php-src/sapi/cli/php_cli.c:1362
    

    Code is (random mdb file from https://github.com/mdbtools/mdbtestdata/blob/master/data/nwind.mdb to placate it):

    <?php
    $file = "/home/calvin/nwind.mdb";
    $dsn = "odbc:Driver=MDBTools;DBQ=$file";
    $pdo = new \PDO($dsn);

    (edit: unixODBC trace shows the crash in the freeing of the environment handle)

  4. NattyNarwhal commented on Dec 18, 2025

    @NattyNarwhal
    Member

    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:

    (gdb) bt
    #0  0x00003ffff79e0fc0 in return_to_pool (connection=connection@entry=0x10225d6b0)
        at /usr/src/debug/dev-db/unixODBC-2.3.14/unixODBC-2.3.14/DriverManager/SQLConnect.c:3957
    #1  0x00003ffff79e486c in SQLDisconnect (connection_handle=0x10225d6b0)
        at /usr/src/debug/dev-db/unixODBC-2.3.14/unixODBC-2.3.14/DriverManager/SQLDisconnect.c:293
    #2  0x00000001005e0c38 in odbc_handle_closer (dbh=0x3ffff745c240) at /home/calvin/php-src/ext/pdo_odbc/odbc_driver.c:134
    #3  0x00000001005ce234 in dbh_free (dbh=0x3ffff745c240, free_persistent=false) at /home/calvin/php-src/ext/pdo/pdo_dbh.c:1562
    #4  0x00000001005ce6a0 in pdo_dbh_free_storage (std=0x3ffff74951e8) at /home/calvin/php-src/ext/pdo/pdo_dbh.c:1611
    #5  0x0000000100bd9784 in zend_objects_store_del (object=0x3ffff74951e8) at /home/calvin/php-src/Zend/zend_objects_API.c:196
    #6  0x0000000100c06bdc in rc_dtor_func (p=0x3ffff74951e8) at /home/calvin/php-src/Zend/zend_variables.c:57
    #7  0x0000000100c06afc in i_zval_ptr_dtor (zval_ptr=0x3fffffffb098) at /home/calvin/php-src/Zend/zend_variables.h:45
    #8  0x0000000100c06e5c in zval_ptr_dtor (zval_ptr=0x3fffffffb098) at /home/calvin/php-src/Zend/zend_variables.c:84
    #9  0x0000000100b4620c in _zend_hash_del_el_ex (ht=0x101fb4c00, idx=9, p=0x3ffff745d320, prev=0x0) at /home/calvin/php-src/Zend/zend_hash.c:1500
    #10 0x0000000100b46370 in _zend_hash_del_el (ht=0x101fb4c00, idx=9, p=0x3ffff745d320) at /home/calvin/php-src/Zend/zend_hash.c:1527
    #11 0x0000000100b491f8 in zend_hash_reverse_apply (ht=0x101fb4c00, apply_func=@0x101e66d08: 0x100a4ceb8 <zval_call_destructor>)
        at /home/calvin/php-src/Zend/zend_hash.c:2243
    #12 0x0000000100a4d230 in shutdown_destructors () at /home/calvin/php-src/Zend/zend_execute_API.c:262
    #13 0x0000000100c16588 in zend_call_destructors () at /home/calvin/php-src/Zend/zend.c:1342
    #14 0x00000001008e9234 in php_request_shutdown (dummy=0x0) at /home/calvin/php-src/main/main.c:1985
    #15 0x0000000100c1e028 in do_cli (argc=2, argv=0x101fb1540) at /home/calvin/php-src/sapi/cli/php_cli.c:1158
    #16 0x0000000100c1e6d4 in main (argc=2, argv=0x101fb1540) at /home/calvin/php-src/sapi/cli/php_cli.c:1362
    

    I'm not entirely sure if this is more of an MDBTools/unixODBC bug and PHP is just the messenger being shot here too.

  5. NattyNarwhal commented on Dec 18, 2025

    @NattyNarwhal
    Member

    I think this is an interaction between mdbtools and unixODBC's connection pooling:

    calvin@php-ci-ppc64be ~/php-src $ sapi/cli/php -d pdo_odbc.connection_pooling=off ~/mdbtools.php 
    calvin@php-ci-ppc64be ~/php-src $ sapi/cli/php ~/mdbtools.php 
    Segmentation fault         (core dumped) sapi/cli/php ~/mdbtools.php
    

    I also can't reproduce this with another ODBC driver (i.e. MySQL's).

  6. matyhtf commented on Dec 19, 2025

    @matyhtf
    ContributorAuthor

    Version:

    3c732aa18f85:/work# php -v
    PHP 8.4.14 (cli) (built: Oct 24 2025 20:05:56) (NTS)
    Copyright (c) The PHP Group
    Zend Engine v4.4.14, Copyright (c) Zend Technologies
        with Zend OPcache v8.4.14, Copyright (c), by Zend Technologies
    3c732aa18f85:/work# odbc_config --longodbcversion
    3.52
    3c732aa18f85:/work# odbc_config --version
    2.3.12
    3c732aa18f85:/work# apk info unixodbc
    unixodbc-2.3.12-r0 description:
    ODBC is an open specification to access Data Sources
    
    unixodbc-2.3.12-r0 webpage:
    https://www.unixodbc.org/
    
    unixodbc-2.3.12-r0 installed size:
    655 KiB
    
  7. NattyNarwhal commented on Dec 19, 2025

    @NattyNarwhal
    Member

    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.

  8. 1 remaining item

  9. matyhtf commented on Dec 23, 2025

    @matyhtf
    ContributorAuthor

    After disabling the connection pool, there was no crash.

    cat /etc/php84/conf.d/01_pdo_odbc.ini
    extension=pdo_odbc
    pdo_odbc.connection_pooling=off
    
    USE_ZEND_ALLOC=0 valgrind php mdb.php
    ==41930== Memcheck, a memory error detector
    ==41930== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
    ==41930== Using Valgrind-3.24.0 and LibVEX; rerun with -h for copyright info
    ==41930== Command: php mdb.php
    ==41930== 
    ==41930== Warning: client switching stacks?  SP change: 0x1ffeffd378 --> 0x11425ff8
    ==41930==          to suppress, use: --max-stackframe=137132602240 or greater
    ==41930== Warning: client switching stacks?  SP change: 0x11425488 --> 0x1ffeffd378
    ==41930==          to suppress, use: --max-stackframe=137132605168 or greater
    ==41930== Warning: client switching stacks?  SP change: 0x1ffeffce48 --> 0x11425488
    ==41930==          to suppress, use: --max-stackframe=137132603840 or greater
    ==41930==          further instances of this message will not be shown.
    object(PDO)#3 (0) {
    }
    ==41930== Warning: invalid file descriptor -1 in syscall close()
    ==41930== 
    ==41930== HEAP SUMMARY:
    ==41930==     in use at exit: 80,640 bytes in 101 blocks
    ==41930==   total heap usage: 64,079 allocs, 63,978 frees, 18,056,963 bytes allocated
    ==41930== 
    ==41930== LEAK SUMMARY:
    ==41930==    definitely lost: 0 bytes in 0 blocks
    ==41930==    indirectly lost: 0 bytes in 0 blocks
    ==41930==      possibly lost: 0 bytes in 0 blocks
    ==41930==    still reachable: 80,640 bytes in 101 blocks
    ==41930==         suppressed: 0 bytes in 0 blocks
    ==41930== Rerun with --leak-check=full to see details of leaked memory
    ==41930== 
    ==41930== For lists of detected and suppressed errors, rerun with: -s
    ==41930== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
    3c732aa18f85:/work/examples/runtime#
  10. added a commit that references this issue on Jun 29, 2026
    13e1da3
  11. iliaal commented on Jun 29, 2026

    @iliaal
    Member

    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:

    SQLFreeHandle(SQL_HANDLE_DBC, H->dbc);   // child
    ...
    SQLFreeHandle(SQL_HANDLE_ENV, H->env);   // parent

    That's the correct ODBC order: the environment is the parent of the connection, so it has to be freed last. Freeing SQL_HANDLE_ENV first, 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.

  12. added a commit that references this issue on Jun 29, 2026
    a5a9e9e
  13. added a commit that references this issue on Jun 30, 2026
    c8d90e7
  14. added a commit that references this issue on Jun 30, 2026
    c0872ea
  15. reopened this on Jun 30, 2026
  16. added a commit that references this issue on Jun 30, 2026
    92c179e
  17. iliaal commented on Jun 30, 2026

    @iliaal
    Member

    Reopening. The merged patch only guards the NULL-credential connect path against the pooling strcmp() crash. The original odbc_handle_closer use-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 as Refs deliberately.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions