Bug #80909 [Opn->Csd]: Memory leak and possible double free using PDO_ODBC

From: Date: Mon, 21 Feb 2022 11:47:59 +0000
Subject: Bug #80909 [Opn->Csd]: Memory leak and possible double free using PDO_ODBC
References: 1  Groups: php.bugs 
Request: Send a blank email to php-bugs+get-239921@lists.php.net to get a copy of this message
Edit report at https://bugs.php.net/bug.php?id=80909&edit=1

 ID:                 80909
 Updated by:         git@php.net
 Reported by:        calvin at cmpct dot info
 Summary:            Memory leak and possible double free using PDO_ODBC
-Status:             Open
+Status:             Closed
 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:

Automatic comment on behalf of NattyNarwhal (author) and cmb69 (committer)
Revision: https://github.com/php/php-src/commit/dfd2a80b69d469d879d60c12c1d9d9ca49cb3f76
Log: Fix #80909: crash with persistent connections in PDO_ODBC


Previous Comments:
------------------------------------------------------------------------
[2022-02-15 16:27:56] calvin at cmpct dot info

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
```

------------------------------------------------------------------------
[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.

------------------------------------------------------------------------


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


Thread (10 messages)

« previous php.bugs (#239921) next »