Edit report at https://bugs.php.net/bug.php?id=80909&edit=1
ID: 80909
Comment by: calvin at cmpct dot info
Reported by: calvin at cmpct dot info
Summary: Memory leak and possible double free using PDO_ODBC
Status: Open
Type: Bug
Package: PDO ODBC
Operating System: Debian 9
PHP Version: master-Git-2021-03-26 (Git)
Block user comment: N
Private report: N
New Comment:
I think I have enough to explain how this happens:
1. PDO constructs a new connection string with spprintf, replaces and frees the old one
2. When dbh_free is called, the refcount is 2, but it hits the persistent connection early return,
presumably for reuse
```
Breakpoint 2, dbh_free (dbh=0x186e180, free_persistent=false) at
/home/calvin/src/php-src/ext/pdo/pdo_dbh.c:1432
1432 if (dbh->query_stmt) {
(gdb) p *dbh
$4 = {methods = 0x13707c0 <odbc_methods>, driver_data = 0x18ce950, username = 0x199e6b0
"lobsters", password = 0x19a5480 "password", is_persistent = 1, auto_commit = 1,
is_closed = 0, alloc_own_columns = 1, in_txn = false,
max_escaped_char_length = 0, oracle_nulls = 0, stringify = 0, skip_param_evt = 0, _reserved_flags
= 0, data_source = 0x7ffff7685300
"Driver=MariaDB;Database=lobsters_dev;UID=lobsters;PWD=password", data_source_len = 36,
error_code = "00000", error_mode = PDO_ERRMODE_EXCEPTION, native_case =
PDO_CASE_NATURAL, desired_case = PDO_CASE_NATURAL, persistent_id = 0x19a5400
"PDO:DBH:DSN=odbc:Driver=MariaDB;Database=lobsters_dev:lobsters:password",
persistent_id_len = 71, refcount = 2, cls_methods = {0x0, 0x0}, driver = 0x1370920
<pdo_odbc_driver>, def_stmt_ce = 0x194b610, def_stmt_ctor_args = {value = {lval = 0, dval = 0,
counted = 0x0, str = 0x0, arr = 0x0, obj = 0x0,
res = 0x0, ref = 0x0, ast = 0x0, zv = 0x0, ptr = 0x0, ce = 0x0, func = 0x0, ww = {w1 = 0, w2 =
0}}, u1 = {type_info = 0, v = {type = 0 '\000', type_flags = 0 '\000', u =
{extra = 0}}}, u2 = {next = 0, cache_slot = 0,
opline_num = 0, lineno = 0, num_args = 0, fe_pos = 0, fe_iter_idx = 0, property_guard = 0,
constant_flags = 0, extra = 0}}, query_stmt = 0x0, query_stmt_zval = {value = {lval = 0, dval = 0,
counted = 0x0, str = 0x0, arr = 0x0,
obj = 0x0, res = 0x0, ref = 0x0, ast = 0x0, zv = 0x0, ptr = 0x0, ce = 0x0, func = 0x0, ww =
{w1 = 0, w2 = 0}}, u1 = {type_info = 0, v = {type = 0 '\000', type_flags = 0
'\000', u = {extra = 0}}}, u2 = {next = 0, cache_slot = 0,
opline_num = 0, lineno = 0, num_args = 0, fe_pos = 0, fe_iter_idx = 0, property_guard = 0,
constant_flags = 0, extra = 0}}, default_fetch_type = PDO_FETCH_BOTH}
(gdb) p dbh->refcount
$5 = 2
(gdb) p free_persistent
$6 = false
(gdb) next
1437 if (dbh->is_persistent) {
(gdb) next
1439 ZEND_ASSERT(!free_persistent || (dbh->refcount == 1));
(gdb) next
1441 if (!free_persistent && (--dbh->refcount)) {
(gdb) next
1442 return;
(gdb) p free_persistent
$7 = false
(gdb) p dbh->refcount
$8 = 1
```
This means the pefree for dbh->data_source never gets called when we want it to.
3. But this is CLI, so we're exiting anyways. It seems the PDO object is freed during the check
for leaks, and the crash happens here inside of glibc (mixed up allocator? double free?)
```
Starting program: /tmp/php/bin/php ../test-pdo-odbc-mariadb.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
[Tue Feb 15 12:06:10 2022] Script: '/home/calvin/src/test-pdo-odbc-mariadb.php'
/home/calvin/src/php-src/Zend/zend_smart_str.c(164) : Freeing 0x00007ffff7685300 (224 bytes),
script=/home/calvin/src/test-pdo-odbc-mariadb.php
=== Total 1 memory leaks detected ===
Breakpoint 1, dbh_free (dbh=0x186e180, free_persistent=true) at
/home/calvin/src/php-src/ext/pdo/pdo_dbh.c:1451
1451 pefree((char *)dbh->data_source, dbh->is_persistent);
(gdb) step
__GI___libc_free (mem=0x7ffff7685300) at malloc.c:3240
Downloading -0.00 MB source file /usr/src/debug/glibc-2.34-25.fc35.x86_64/malloc/malloc.c
3240 {
(gdb) step
3244 if (mem == 0) /* free(0) has no effect */
(gdb) step
3252 int err = errno;
(gdb) step
3256 if (chunk_is_mmapped (p)) /* release mmapped memory. */
(gdb) step
3260 if (!mp_.no_dyn_threshold
(gdb) step
3269 munmap_chunk (p);
(gdb) step
munmap_chunk (p=0x7ffff76852f0) at malloc.c:2935
2935 size_t pagesize = GLRO (dl_pagesize);
(gdb) next
2936 INTERNAL_SIZE_T size = chunksize (p);
(gdb) next
2938 assert (chunk_is_mmapped (p));
(gdb) next
2941 uintptr_t block = (uintptr_t) p - prev_size (p);
(gdb) next
2942 size_t total_size = prev_size (p) + size;
(gdb) next
2948 if (__glibc_unlikely ((block | total_size) & (pagesize - 1)) != 0
(gdb) next
2950 malloc_printerr ("munmap_chunk(): invalid pointer");
(gdb) next
munmap_chunk(): invalid pointer
```
Previous Comments:
------------------------------------------------------------------------
[2022-02-15 15:17:58] calvin at cmpct dot info
I can confirm it's the constructed connection string (if you supply UID/PWD as args for the PDO
ctor instead of on the connection string itself) that's leaking:
```
(gdb) break main.c:1604
Breakpoint 1 at 0x965f35: file /home/calvin/src/php-src/main/main.c, line 1604.
(gdb) run
Starting program: /tmp/php/bin/php ../test-pdo-odbc-mariadb.php
This GDB supports auto-downloading debuginfo from the following URLs:
https://debuginfod.fedoraproject.org/
Enable debuginfod for this session? (y or [n]) y
Debuginfod has been enabled.
To make this setting permanent, add 'set debuginfod enabled on' to .gdbinit.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
[Tue Feb 15 11:15:16 2022] Script: '/home/calvin/src/test-pdo-odbc-mariadb.php'
Breakpoint 1, php_message_handler_for_zend (message=4, data=0x7fffffffbe70) at
/home/calvin/src/php-src/main/main.c:1604
1604 snprintf(memory_leak_buf, 512, "%s(%" PRIu32 ") : Freeing "
ZEND_ADDR_FMT " (%zu bytes), script=%s\n", t->filename, t->lineno,
(size_t)t->addr, t->size, SAFE_FILENAME(SG(request_info).path_translated));
(gdb) p t
$1 = (zend_leak_info *) 0x7fffffffbe70
(gdb) p *t
$2 = {addr = 0x7ffff7685300, size = 224, filename = 0x14bf478
"/home/calvin/src/php-src/Zend/zend_smart_str.c", orig_filename = 0x0, lineno = 164,
orig_lineno = 0}
(gdb) p (char*)0x7ffff7685300
$3 = 0x7ffff7685300
"Driver=MariaDB;Database=<database>;UID=<UID>;PWD=<Password>"
```
------------------------------------------------------------------------
[2022-02-15 15:07:46] calvin at cmpct dot info
FWIW, I can still reproduce this on 8.2 master (b582427ff53db38cac3e23d3c990814da418038c), but I
think the symptom might have changed.
Test program using MariaDB's ODBC driver (so we can discount IBM's weird driver), running
on Fedora 35:
```
<?php
$connection = new PDO('odbc:Driver=MariaDB;Database=<DB here>',
'username', 'password', array(PDO::ATTR_PERSISTENT => true));
```
Gets:
```
$ /tmp/php/bin/php ../test-pdo-odbc-mariadb.php
[Tue Feb 15 11:04:47 2022] Script: '/home/calvin/src/test-pdo-odbc-mariadb.php'
/home/calvin/src/php-src/Zend/zend_smart_str.c(164) : Freeing 0x00007f2daa285300 (224 bytes),
script=/home/calvin/src/test-pdo-odbc-mariadb.php
=== Total 1 memory leaks detected ===
munmap_chunk(): invalid pointer
Aborted (core dumped)
```
The address of the leaked pointer changes, and it only leaks if the connection is successful; it
will always crash with munmap_chunk() regardless. USE_ZEND_ALLOC=0 seems to make it work, but
probably by covering it up.
------------------------------------------------------------------------
[2021-05-04 22:48:55] calvin at cmpct dot info
I'm poking this in GDB and I think it's the connection string (or a chunk of it)
that's getting leaked. Transcript from my session: https://gist.githubusercontent.com/NattyNarwhal/69359a88979e254b6f9eb9e91512c522/raw/ddeed94e65f4401092b9ba8586dfc883b9c44fd0/gistfile1.txt
------------------------------------------------------------------------
[2021-05-04 20:16:11] calvin at cmpct dot info
Just FWIW, I can reproduce this issue on Fedora with MariaDB's ODBC driver, and with all other
drivers disabled in odbcinst.ini. I don't think this is an ODBC driver issue as a result.
------------------------------------------------------------------------
[2021-03-30 18:24:59] calvin at cmpct dot info
Let me know if you need access to a system/the driver. I wonder if this is a possible IBM driver bug
and PHP is taking the blame here, but it's odd I can only repro on Linux if so.
------------------------------------------------------------------------
The remainder of the comments for this report are too long. To view
the rest of the comments, please view the bug report online at
https://bugs.php.net/bug.php?id=80909
--
Edit this bug report at https://bugs.php.net/bug.php?id=80909&edit=1