Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ext/pdo: Convert def_stmt_ctor_args field to Hashtable* #12154

Closed
wants to merge 4 commits into from

Conversation

Girgias
Copy link
Member

@Girgias Girgias commented Sep 8, 2023

Take 2 on #9803 now that the FCI/FCC thing has been converted separately.

Comment on lines 830 to 824
ZVAL_COPY(&dbh->def_stmt_ctor_args, item);
/* Separate array */
dbh->def_stmt_ctor_args = zend_array_dup(Z_ARRVAL_P(item));
Copy link
Member

Choose a reason for hiding this comment

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

Why do you need to duplicate the array? Previously we just incremented reference counter.

Copy link
Member Author

Choose a reason for hiding this comment

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

Looks like I was double freeing the HashTable, once manually in the free handler, and another time indirectly via the GC. Let me know if the new code still makes sense.

Copy link
Member

@dstogov dstogov left a comment

Choose a reason for hiding this comment

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

See the question about zend_array_dup(). I don't see other problems.

@Girgias Girgias force-pushed the pdo-ctor_args-hashtablr branch from b13a1f9 to 501d0a3 Compare September 11, 2023 12:17
if (!Z_ISUNDEF(dbh->def_stmt_ctor_args)) {
zval_ptr_dtor(&dbh->def_stmt_ctor_args);
}
/* The GC, via dbh_get_gc(), will free dbh->def_stmt_ctor_args() */
Copy link
Member

@SakiTakamachi SakiTakamachi Sep 11, 2023

Choose a reason for hiding this comment

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

[IMHO]

It might be better to free dbh->def_stmt_ctor_args here as well.

Code like the following will cause memory leaks because dbh_get_gc is not called:

<?php
$db = new PDO('mysql:host=mysql;dbname=test;', 'root', ''); // my env
class Foo extends PDOStatement {}
$db->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('Foo', ['param1']));

gdb:

(gdb) b dbh_get_gc
Breakpoint 1 at 0x364ad9: file /php-src/ext/pdo/pdo_dbh.c, line 1312.
(gdb) run /var/www/html/test_12154.php
Starting program: /php-src/sapi/cli/php /var/www/html/test_12154.php
[Mon Sep 11 15:25:29 2023]  Script:  '/var/www/html/test_12154.php'
/php-src/Zend/zend_hash.c(291) :  Freeing 0x00007ffff7a02600 (56 bytes), script=/var/www/html/test_12154.php
[Mon Sep 11 15:25:29 2023]  Script:  '/var/www/html/test_12154.php'
/php-src/Zend/zend_string.h(176) :  Freeing 0x00007ffff7a045c0 (32 bytes), script=/var/www/html/test_12154.php
[Mon Sep 11 15:25:29 2023]  Script:  '/var/www/html/test_12154.php'
/php-src/Zend/zend_hash.c(157) :  Freeing 0x00007ffff7a5e180 (136 bytes), script=/var/www/html/test_12154.php
=== Total 3 memory leaks detected ===
[Inferior 1 (process 4956) exited normally]

Copy link
Member Author

Choose a reason for hiding this comment

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

Right, I was wondering about this. I'll add some tests for a GC and the non-GC case which made me think this (an SQLite bug)

@Girgias Girgias force-pushed the pdo-ctor_args-hashtablr branch from 501d0a3 to 7038e67 Compare September 12, 2023 02:55
ZVAL_COPY(&dbh->def_stmt_ctor_args, item);
dbh->def_stmt_ctor_args = Z_ARRVAL_P(item);
/* Increase refcount */
GC_TRY_ADDREF(dbh->def_stmt_ctor_args);
Copy link
Member

@SakiTakamachi SakiTakamachi Sep 12, 2023

Choose a reason for hiding this comment

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

Isn't increasing the count here the cause of the memory leak?

Passing it to the constructor increments the ref count, so I thought maybe there was no need to increment the count at this point, so I commented it out and tried running the following code.
Memory leak no longer occurs.

<?php
$db = new PDO('mysql:host=mysql;dbname=test;', 'root', ''); // my env
class Foo extends PDOStatement {
    private function __construct($v) {
        var_dump($v);
    }
}
$db->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('Foo', ['param1']));

$stmt = $db->query('SELECT 1');
var_dump($stmt);

result:

string(6) "param1"
object(Foo)#2 (1) {
  ["queryString"]=>
  string(8) "SELECT 1"
}

I also tried this as well.
This does not leak memory to begin with, but I tried it to confirm behavior.

<?php
class Foo extends PDOStatement {
    private function __construct($v) {
        var_dump($v);
    }
}

class Bar extends PDO {
    public $statementClass = 'Foo';
    function __construct($dsn, $username, $password, $driver_options = []) {
        $driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
        parent::__construct($dsn, $username, $password, $driver_options);

        $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, [$this->statementClass, [$this]]);
    }
}

$db = new Bar('mysql:host=mysql;dbname=test;', 'root', ''); // my env

$stmt = $db->query('SELECT 1');
var_dump($stmt);

result:

object(Bar)#1 (1) {
  ["statementClass"]=>
  string(3) "Foo"
}
object(Foo)#2 (1) {
  ["queryString"]=>
  string(8) "SELECT 1"
}

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think not incrementing the reference counter is the correct approach.

As something along the lines of:

<?php
if (getenv('REDIR_TEST_DIR') === false) putenv('REDIR_TEST_DIR='.__DIR__ . '/../../pdo/tests/');
require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';

class Foo extends PDOStatement {
    private function __construct($v) {
        var_dump($v);
    }
}

$db = PDOTest::factory();

$a = ['Foo', ['param1']];
$db->setAttribute(PDO::ATTR_STATEMENT_CLASS, $a);
unset($a);
$stmt = $db->query('SELECT 1');
var_dump($stmt);

?>

Should still work, I'm pretty sure this is why I did a dup.

Copy link
Member Author

Choose a reason for hiding this comment

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

Also just removing this line causes use after frees so just that is definitely not the solution.

Copy link
Member

@SakiTakamachi SakiTakamachi Sep 14, 2023

Choose a reason for hiding this comment

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

@Girgias

Passing it to the constructor increments the ref count, so I thought maybe there was no need to increment the count at this point

This was clearly a mistake.
However, after looking around, I still don't think I should increment the refcount here, for other reasons.

The test code you provided that includes unset worked fine even after removing GC_TRY_ADDREF.

dbh->def_stmt_ctor_args = Z_ARRVAL_P(item); is a copy, so whatever happens to $a after this should have no effect at all (of course, there is no problem using unset()).

Sorry, the above is also incorrect.


In the following case, at the time of setAttribute, refcount is 2, and unset() reduces it to 1.
(with removing GC_TRY_ADDREF. I checked by gdb.)

<?php
$db = new PDO('mysql:host=mysql;dbname=test;', 'root', '');
class Foo extends PDOStatement {
    private function __construct($v) {
        var_dump($v);
    }
}
$a = ['Foo', ['param1']];
$db->setAttribute(PDO::ATTR_STATEMENT_CLASS, $a); // refcount 2
unset($a[1]);

$stmt = $db->query('SELECT 1'); // refcount 1
var_dump($stmt);

Copy link
Member

@SakiTakamachi SakiTakamachi Sep 14, 2023

Choose a reason for hiding this comment

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

If I remove GC_TRY_ADDREF, ht will be freed by the next process and then used again.

if (dbh->def_stmt_ctor_args) {
	zend_array_release(dbh->def_stmt_ctor_args);
	dbh->def_stmt_ctor_args = NULL;
}
// If I try to set the exact same thing twice, a problem will occur.
$db->setAttribute(PDO::ATTR_STATEMENT_CLASS, ['Foo', ['param1']]);
$db->setAttribute(PDO::ATTR_STATEMENT_CLASS, ['Foo', ['param1']]);

So in this case, it definitely need an GC_TRY_ADDREF here, I understood it.

@Girgias Girgias force-pushed the pdo-ctor_args-hashtablr branch from 7038e67 to 2100104 Compare September 13, 2023 13:52
Comment on lines 1379 to 1390
/* When the GC is running, the ctor_args will be added to the GC buffer marked to be released.
* However, before freeing the buffer, it will release the object by calling zend_objects_store_del()
* which calls pdo_dbh_free_storage() which in turn calls this function.
* Thus we must not free the ctor_args when the GC is running. */
if (dbh->def_stmt_ctor_args) {
zend_gc_status gc_status;
zend_gc_get_status(&gc_status);

if (EXPECTED(!gc_status.active)) {
zend_array_release(dbh->def_stmt_ctor_args);
dbh->def_stmt_ctor_args = NULL;
}
Copy link
Member

@arnaud-lb arnaud-lb Sep 16, 2023

Choose a reason for hiding this comment

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

The GC avoids this issue by setting GC_TYPE_INFO() to IS_NULL here and here so that zval_ptr_dtor() is a no-op on that value when it's already being destroyed.

Checking GC_TYPE(dbh->def_stmt_ctor_args) before calling zend_array_release(dbh->def_stmt_ctor_args) would fix the issue:

	if (dbh->def_stmt_ctor_args && GC_TYPE(dbh->def_stmt_ctor_args) == IS_ARRAY) {
		zend_array_release(dbh->def_stmt_ctor_args);

Alternatively we could use GC_DTOR(), which is equivalent to zval_ptr_dtor for refcounted values, but it may unnecessarily call gc_check_possible_root. We could introduce a new macro GC_DTOR_NOGC that does not call gc_check_possible_root, like zval_ptr_dtor_nogc.

@Girgias Girgias force-pushed the pdo-ctor_args-hashtablr branch 2 times, most recently from f2e363a to 15b2ffd Compare September 16, 2023 19:02
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.

4 participants