-
Notifications
You must be signed in to change notification settings - Fork 7.8k
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
Migrate ext/odbc resources to opaque objects #12040
Conversation
de3adc0
to
6b8012f
Compare
ext/odbc/php_odbc.c
Outdated
int i; | ||
|
||
if (res->values) { | ||
for(i = 0; i < res->numcols; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be as follows:
int i; | |
if (res->values) { | |
for(i = 0; i < res->numcols; i++) { | |
if (res->values) { | |
for (int i = 0; i < res->numcols; i++) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI: this is a preexisting piece of code, originally coming from _free_odbc_result
but I'm ok with fixing this.
ext/odbc/php_odbc.c
Outdated
zend_hash_destroy(&ODBCG(results)); | ||
zend_hash_destroy(&ODBCG(connections)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this happen in GSHUTDOWN? Considering they are init in GINIT.
Also if we don't destroy them per request, wouldn't this allow us to also remove the persistent resource type?
Maybe one "generic" way of doing this to check on the persistent flag via a hash_apply function (or whatever the name of those are)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this happen in GSHUTDOWN? Considering they are init in GINIT.
Hmm, great question... I used ext/pgsql as a reference where the same happens.. According to what I could distill from the externals book, the main difference between MINIT and GINIT is this:
GINIT() is launched before MINIT() for the current process. In case of NTS, that’s all. In case of ZTS, GINIT() will be called additionally for every new thread spawned by the thread library.
So I guess I should rather destroy the hashes in GSHUTDOWN
if I'm not mistaken...
Also if we don't destroy them per request, wouldn't this allow us to also remove the persistent resource type?
Yeah, this crossed my mind as well, but I'm not sure about it, as the same was done in case of mysqli and pgsql as well: #6791 (comment) But we should definitely discuss the feasibility of your idea with the PHPF. :)
RETURN_THROWS(); | ||
} | ||
result = Z_ODBC_RESULT_P(pv_res); | ||
CHECK_ODBC_RESULT(result); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Drive by remark for bellow, the strcasecmp()
call looks fishy as it doesn't deal with binary strings which might be passed as the fname
value from userland
} | ||
conn = Z_ODBC_CONNECTION_P(pv_handle); | ||
CHECK_ODBC_CONNECTION(conn); | ||
|
||
if (mode == 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This parameter name is really bad as I was confused where the hell the mode
value came from, as I didn't see it from the userland call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks more or less good, except for the "permutation changes" that I can't follow.
ext/odbc/php_odbc_includes.h
Outdated
@@ -240,8 +246,8 @@ ZEND_BEGIN_MODULE_GLOBALS(odbc) | |||
zend_long default_cursortype; | |||
char laststate[6]; | |||
char lasterrormsg[SQL_MAX_MESSAGE_LENGTH]; | |||
HashTable *resource_list; | |||
HashTable *resource_plist; | |||
HashTable results; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to keep results?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the list is needed in two places:
- all results are closed when
odbc_close_all()
is called - when a connection is closed, its related results are closed as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I see it well, the results are not yet added anywhere to the hash table...
odbc_param_info * param_info; | ||
odbc_param_info *param_info; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would recommend not to mix white-space and unrelated changes with a big patch. You may commit the obvious changes before.
ext/odbc/php_odbc_includes.h
Outdated
zend_resource *res; | ||
int persistent; | ||
} odbc_connection; | ||
|
||
typedef struct odbc_link { | ||
odbc_connection *connection; | ||
zend_string *hash; | ||
bool persistent; | ||
zend_object std; | ||
} odbc_link; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would keep persistent
flag in odbc_connection
.
May be I didn't understand why it was moved...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's easier to access it in the link structure, and we don't need it in the connection. However, I can bring it back if you want. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't care.
8f743f1
to
756b7e1
Compare
@@ -316,31 +316,6 @@ static const func_info_t func_infos[] = { | |||
FN("oci_get_implicit_resultset", MAY_BE_RESOURCE|MAY_BE_FALSE), | |||
FN("oci_password_change", MAY_BE_RESOURCE|MAY_BE_BOOL), | |||
FN("oci_new_cursor", MAY_BE_RESOURCE|MAY_BE_FALSE), | |||
FN("odbc_prepare", MAY_BE_RESOURCE|MAY_BE_FALSE), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I know, these always return a new instance, thus they can be annotated as @refcount 1
, cannot they?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you rebase this just to check nothing is broken, and add the UPGRADING entries?
Otherwise this LGTM.
756b7e1
to
96b62de
Compare
@NattyNarwhal Could you please have a look at this PR before I merge it? |
I'm on vacation this week, but I'll try to review this Monday night. |
I haven't been able to set up an environment to test with yet (I'll probably give it another pass because of this), but I did go over the changes beyond the mechanical changing to an object - they're sensible refactors in general. |
ext/odbc/odbc.stub.php
Outdated
function odbc_connection_string_should_quote(string $str): bool {} | ||
|
||
function odbc_connection_string_quote(string $str): string {} | ||
namespace ODBC { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If follow the naming convention, wouldn't it be Odbc
?
Namespace naming conventions were irrelevant
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, yes, namespaces have different rules, but I am fine to rename it, especially because Tim's https://wiki.php.net/rfc/class-naming-acronyms will hopefully/likely pass.
odbc_result *result; | ||
RETCODE rc; | ||
zval *pv_handle; | ||
zend_long pv_which, pv_opt, pv_val; | ||
|
||
if (zend_parse_parameters(ZEND_NUM_ARGS(), "rlll", &pv_handle, &pv_which, &pv_opt, &pv_val) == FAILURE) { | ||
if (zend_parse_parameters(ZEND_NUM_ARGS(), "olll", &pv_handle, &pv_which, &pv_opt, &pv_val) == FAILURE) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe?
if (zend_parse_parameters(ZEND_NUM_ARGS(), "olll", &pv_handle, &pv_which, &pv_opt, &pv_val) == FAILURE) { | |
if (zend_parse_parameters(ZEND_NUM_ARGS(), "Olll", &pv_handle, odbc_connection_ce, &pv_which, &pv_opt, &pv_val) == FAILURE) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not possible unfortunately, since the function also accepts a result_ce
when the 3rd argument has a value of 2
.
Finally back from vacation, so I can test this. FWIW, I know of at least one library (i.e. zendtech/ibmitoolkit) using However, I've been testing the behaviour with the library right now, and I'm getting heap corruption with macOS 14.4/arm64 and IBM's Db2i driver, and I can't reproduce it with current master:
without zend_alloc:
(My configuration invocation for this is Sample program: <?php
// composer require zendtech/ibmitoolkit
require("vendor/autoload.php");
use ToolkitApi\Toolkit;
// set $cs/$u/$p to some system with i and the i ODBC driver, i.e.
$cs = "Driver=IBM i Access ODBC Driver;System=pub400.com;CCSID=1208";
$u = "";
$p = "";
$odbc = odbc_connect($cs, $u, $p);
var_dump($odbc);
$tkobj = ToolkitService::getInstance($odbc, "", "", "odbc");
$tkobj->setOptions(array('stateless' => true));
$result = $tkobj->ClInteractiveCommand('DSPLIBL');
print_r($result); |
I can reproduce it on Fedora 39/amd64 as well, quick run of Valgrind: https://gist.githubusercontent.com/NattyNarwhal/b243f16a2ba8bb8c4dbcc8d6bd825c3e/raw/12d8ab7153ae740efc23e05e962a89867b5e4a5a/gistfile1.txt |
@NattyNarwhal thank you for your help! Fortunately, I could also reproduce the failures on my M3 Mac, and I have already managed to fix most of the problems locally, but ASAN still keeps on bringing up a few more similar issues (double frees mostly). Also, there may have been some conflict resolution issue 🤔 i hope that I can push my changes during the weekend. |
96b62de
to
b08b07e
Compare
Huh, there were a lot of errors, but now it looks like tests are fixed locally. I happen to notice some leaks when I'm using the debugger, so I'll try to have a look at them as well. I'll leave review comments soon about what and why I had to change. |
b08b07e
to
91c1393
Compare
link->connection = NULL; | ||
ODBCG(num_links)--; | ||
if (!link->persistent) { | ||
ZEND_ASSERT(link->connection && "link has already been closed"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added this assert in order to be clear when this function can be used.
ext/odbc/php_odbc.c
Outdated
if (link->persistent) { | ||
zend_hash_del(&EG(persistent_list), link->hash); | ||
} else { | ||
close_results_with_connection(link->connection); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can now close all the results related to the relevant connection before it itself is freed.
ext/odbc/php_odbc.c
Outdated
zend_string_release(link->hash); | ||
link->hash = NULL; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't use explicit nulls before, but they were useful in order to prevent double frees.
@@ -154,6 +172,7 @@ static void odbc_result_free(odbc_result *res) | |||
} | |||
efree(res->values); | |||
res->values = NULL; | |||
res->numcols = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm zeroing this member just in case
ext/odbc/php_odbc.c
Outdated
odbc_result *result = odbc_result_from_obj(obj); | ||
|
||
if (!result->stmt) { | ||
if (!result->conn_ptr) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the conn_ptr
member is checked whether a result is open (CHECK_ODBC_RESULT(result);
), I started to use it here as well.
SQLAllocConnect(link->connection->henv, link->connection->hdbc); | ||
link->hash = zend_string_copy(hash); | ||
ret = SQLAllocEnv(&((*link->connection).henv)); | ||
if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added this check when debugging the above mentioned issue. I think we could keep not checking the error condition, and then an error would surface later on anyway, but I guess it's better to catch it as early as possible. I can remove this though if you think it's not needed.
ext/odbc/php_odbc.c
Outdated
return false; | ||
} | ||
|
||
ret = SQLAllocConnect((*link->connection).henv, &((*link->connection).hdbc)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the same here as above
ext/odbc/php_odbc.c
Outdated
@@ -2358,9 +2398,9 @@ void odbc_do_connect(INTERNAL_FUNCTION_PARAMETERS, int persistent) | |||
RETURN_FALSE; | |||
} | |||
ODBCG(num_links)++; | |||
zend_hash_update_ptr(&ODBCG(connections), hashed_details.s, Z_ODBC_LINK_P(return_value)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accidentally, this line used to be in the wrong place a few lines below so that even persistent connections ended up being in the (non-persistent) connection list. :/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why update? What about the old value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also you should document in the header this hashtable does not take ownership of the connection, rather it is a borrow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've just realized the bug here too when answering your other comment 🙂 I'll fix this for sure. Although the previous code didn't use connection reuse, but now we should in order not to have to take care of hash collision. I'm uncertain whether this change would cause any problems for people
ext/odbc/php_odbc.c
Outdated
@@ -2378,12 +2418,6 @@ PHP_FUNCTION(odbc_close) | |||
link = Z_ODBC_LINK_P(pv_conn); | |||
CHECK_ODBC_CONNECTION(link->connection); | |||
|
|||
if (link->persistent) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this block of code is not needed anymore since odbc_link_free()
makes sure to remove any trace of the connection
ext/odbc/php_odbc.c
Outdated
|
||
if (link->persistent) { | ||
zend_hash_del(&EG(persistent_list), link->hash); | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_close_odbc_pconn()
will free most of the stuff when the resource is freed, so we have to avoid doing so again here, that's why I moved these pieces of code in the else
block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is my review so far, and I think you can already fix some leaks with this.
I stopped looking for the moment being. All in all, to be honest, I find the code quite messy (not really your fault ofc, this is the problem with legacy code) with insufficient comments to say why something is the way it is instead of what. In particular, there is no clearly defined model of which structure holds strong references to data vs only borrows data.
HashTable *resource_list; | ||
HashTable *resource_plist; | ||
HashTable results; | ||
HashTable connections; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ODBCG(connections)
is used to store non-persistent connections, so they don't survive across requests, so they should be initialized in RINIT and cleaned up in RSHUTDOWN. That table should also be called non_persistent_connections btw, otherwise it's confusing for reviewers.
Creating a persistent table and then storing non-persistent stuff in it is icky.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a side question, why do we hold such a reference at all? I know this is pre-existing, but I think it makes sense to clean up non-persistent connections as soon as the connection object becomes unreachable instead of waiting until the request ends, no? I do see that odbc_connect
closes both persistent and non-persistent connections, but I find the fact that connections are held even if the connection object isn't reachable from userland very very weird.
The concept just makes everything more difficult than it has to be imho.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I originally put it there AFAIR but I changed for GSHUTDOWN
after this review comment: #12040 (comment) I even looked up https://www.phpinternalsbook.com/php7/extensions_design/php_lifecycle.html#globals-termination-gshutdown and thought that GSHUTDOWN
is called during RSHUTDOWN
assuming that these globals should be freed there, however, I missed this sentence at the very end of the section: Remember that globals are not cleared after every request; aka GSHUTDOWN() is not called as part of RSHUTDOWN().
So you are surely right, I'll use RSHUTDOWN
then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a side question, why do we hold such a reference at all? I know this is pre-existing, but I think it makes sense to clean up non-persistent connections as soon as the connection object becomes unreachable instead of waiting until the request ends, no?
The connections
hash table is needed because there's a odbc_close_all()
function which closes all connections right away. The results
hash table is needed because results are automatically closed as soon as a connection is closed... And I agree with you, I also think that odbc_close_all()
is probably the main source of complexity in this extension... Also, by storing connections in a hash map, it's possible to implement connection reuse if we want to (it is already available for persistent resources).
ext/odbc/php_odbc.c
Outdated
static zend_object_handlers odbc_connection_object_handlers, odbc_result_object_handlers; | ||
|
||
static void odbc_insert_new_result(zval *result) { | ||
Z_TRY_ADDREF_P(result); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we use Z_TRY_ADDREF_P
in odbc_insert_new_result
instead of Z_ADDREF_P
? That's not necessary because it'll always be an object, that would be good to assert btw.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no specific reason TBH, it's copy-pasted from other places where Z_TRY_ADDREF_P
was used. I'll add the assert as well
ext/odbc/php_odbc.c
Outdated
@@ -2358,9 +2398,9 @@ void odbc_do_connect(INTERNAL_FUNCTION_PARAMETERS, int persistent) | |||
RETURN_FALSE; | |||
} | |||
ODBCG(num_links)++; | |||
zend_hash_update_ptr(&ODBCG(connections), hashed_details.s, Z_ODBC_LINK_P(return_value)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why update? What about the old value?
ext/odbc/php_odbc.c
Outdated
} | ||
} ZEND_HASH_FOREACH_END(); | ||
|
||
/* Loop through the results list searching for any dangling results and close all statements */ | ||
ZEND_HASH_FOREACH_VAL(&ODBCG(results), zv) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We only need this for persistent connections right? Please make this clear in the comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not exactly sure what you mean here, but results may have either a persistent or a non-persistent connection
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't odbc_link_free
closing the results via close_results_with_connection
? In that case, this loop only affects persistent connections.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it does, but so does _close_odbc_pconn()
. So the point is rather to close any remaining results which were omitted by any reason. Or it doesn't make sense to do so?
ext/odbc/php_odbc.c
Outdated
@@ -2358,9 +2398,9 @@ void odbc_do_connect(INTERNAL_FUNCTION_PARAMETERS, int persistent) | |||
RETURN_FALSE; | |||
} | |||
ODBCG(num_links)++; | |||
zend_hash_update_ptr(&ODBCG(connections), hashed_details.s, Z_ODBC_LINK_P(return_value)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also you should document in the header this hashtable does not take ownership of the connection, rather it is a borrow.
ext/odbc/php_odbc.c
Outdated
if (!link->connection) { | ||
zend_object_std_dtor(&link->std); | ||
return; | ||
} | ||
|
||
odbc_link_free(link); | ||
zend_object_std_dtor(&link->std); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is the more logical way of writing this, at least it is clearer to me:
if (!link->connection) { | |
zend_object_std_dtor(&link->std); | |
return; | |
} | |
odbc_link_free(link); | |
zend_object_std_dtor(&link->std); | |
if (link->connection) { | |
odbc_link_free(link); | |
} | |
zend_object_std_dtor(obj); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Early-return style is fine and I use it myself. In this particular case however, I found it adds more noise because both paths are quite simple, so by not using early-return style the code becomes easier to read.
ext/odbc/php_odbc.c
Outdated
ZEND_HASH_FOREACH_NUM_KEY_VAL(&ODBCG(results), num_index, p) { | ||
odbc_result *res = Z_ODBC_RESULT_P(p); | ||
if (res->stmt == result->stmt) { | ||
zend_hash_index_del(&ODBCG(results), num_index); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This note is for all occurrences of list modification during a foreach: I would need to check if it is actually always possible to delete during a loop. I remember fixing a weird data corruption issue caused by deleting inside foreach in ext-soap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I was wondering about the same when implementing these foreach based modifications. I'll try to look your changes up and fix accordingly. Since a statement should only be present once in the hash table, I guess the fix is going to be easy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it only occurs once, then you can the removal it in the loop, but use a break;
when the item is removed.
This is what I was referring to: 7e4a323
Although it might be fine in your case as the hashtable is not resized.
91c1393
to
4a7b5f6
Compare
ext/odbc/php_odbc.c
Outdated
return NULL; | ||
zend_hash_next_index_insert_new(connection->results, result); | ||
odbc_result *res = Z_ODBC_RESULT_P(result); | ||
res->index = zend_hash_num_elements(connection->results) - 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I now store the index so that it's faster to remove the item from the hashtable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't right.
It's good to store an index, but the way you assign this is wrong.
Let's say we have three results in the table with indices 0, 1, and 2.
Now we insert an extra result, it gets index 3 because num_elements-1==3. That works.
But now we remove index 0. In the table we have: 1, 2, 3.
num_elements-1==3, so now we have a problem. It should've been index 4.
You should probably be using nNextFreeElement
ext/odbc/php_odbc.c
Outdated
return &intern->std; | ||
} | ||
static void odbc_insert_new_result(odbc_connection *connection, zval *result) { | ||
ZEND_ASSERT(Z_TYPE_P(result) == IS_OBJECT && instanceof_function(Z_OBJCE_P(result), odbc_result_ce)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added the assert for the result type check
ext/odbc/php_odbc.c
Outdated
} | ||
|
||
static void odbc_link_free(odbc_link *link) | ||
{ | ||
ZEND_ASSERT(link->connection && "link has already been closed"); | ||
|
||
if (link->persistent) { | ||
zend_hash_del(&EG(persistent_list), link->hash); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realized that the persistent list only stores the connections themselves so these should only be freed when a connection/all connections are explicitly closed, or during module shutdown. AFAIR I still need to implement freeing persistent connections for odbc_close(). It's done for odbc_close_all() though.
ext/odbc/php_odbc.c
Outdated
static void odbc_insert_new_result(odbc_connection *connection, zval *result) { | ||
ZEND_ASSERT(Z_TYPE_P(result) == IS_OBJECT && instanceof_function(Z_OBJCE_P(result), odbc_result_ce)); | ||
if (!connection->results) { | ||
connection->results = zend_new_array(1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How beneficial is it to only allocate this array lazily? I assume that if you create a connection, you probably want to execute some queries.
If you agree, I'd choose to not lazily allocate this, and instead of having a HashTable*
have a HashTable
this will simplify some code as some NULL checks become irrelevant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I was also wondering exactly about this, whether I should rather use a HashTable
value, since there's a high possibility that a connection will have a result. Thanks for pointing this out!
ext/odbc/php_odbc.c
Outdated
zend_hash_apply_with_argument(&EG(persistent_list), | ||
_close_pconn_with_res, (void *)p); | ||
} | ||
/* Loop through the non-persistent connection list, now close all non-persistent connections and their results */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment such be above the ZEND_HASH_FOREACH_VAL(&ODBCG(non_persistent_connections), zv) {
loop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also normally I'd say to combine the clean
and the loop using ZEND_HASH_FOREACH_END_DEL
at the end, but see #8671 (comment). I guess this is something to keep in mind for a follow-up cleanup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for letting me know about ZEND_HASH_FOREACH_END_DEL()
! I didn't know about it. I'll wait until the fix comes :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, it seems that forward iteration wasn't really intended to work with END_DEL, and while we could use reverse iteration that would be a bit silly. So it's fine to keep this as-is.
ext/odbc/php_odbc.c
Outdated
return NULL; | ||
zend_hash_next_index_insert_new(connection->results, result); | ||
odbc_result *res = Z_ODBC_RESULT_P(result); | ||
res->index = zend_hash_num_elements(connection->results) - 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't right.
It's good to store an index, but the way you assign this is wrong.
Let's say we have three results in the table with indices 0, 1, and 2.
Now we insert an extra result, it gets index 3 because num_elements-1==3. That works.
But now we remove index 0. In the table we have: 1, 2, 3.
num_elements-1==3, so now we have a problem. It should've been index 4.
You should probably be using nNextFreeElement
5b4a1ec
to
24696c0
Compare
1. GC_DELREF isn't enough because the hash table may hold the last reference 2. The reference should be deleted inside the odbc_result_free function to avoid forgetting to call it in other places.
This makes sure we don't get a performance penalty in release mode because the release mode build will still perform the call as the compiler doesn't know instanceof is side-effect-free.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I pushed two commits, one to fix leak (check commit description for reasoning), and one more cleanup-oriented commit.
The final leak is due to the connection object for persistent connections not being freed (which I already commented on a few days ago).
|
||
/* If aborted via timer expiration, don't try to call any unixODBC function */ | ||
if (!(PG(connection_status) & PHP_CONNECTION_TIMEOUT)) { | ||
safe_odbc_disconnect(conn->hdbc); | ||
SQLFreeConnect(conn->hdbc); | ||
SQLFreeEnv(conn->henv); | ||
} | ||
free(conn); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This causes the final leak. You never free the connection object itself for persistent connections.
However, if you do that here, then you will get use-after-frees because there might still be link objects that refer to this.
You should clearly defined in the code via comments what the expected behaviour is of link objects that exist while the connection is already gone (*). And then figure out what to do in that case.
(*) The reason I'm mentioning this is because I find this extension lacking in comments explaining intent and why something is the way it is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the fixes! Yeah, when I was debugging the use-after-free
, and I found out that this free
"causes" it, so I removed it and then I tried to get rid of the memleak regarding result->std
. And sorry for this regression!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now I think I managed to come up with a solution. I'm unsure if there is a better one, but this was my best attempt so far. I explained the idea in the header file, but basically I now started to store persistent links in the ODBC global connection hash table. This way I can keep all the connection
references. And when a connection/all connections are closed, I can execute odbc_link_free()
and _close_odbc_pconn()
in the right order so that the persistent connection itself is always accessible inside the former function and only freed in the latter call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your strategy seems fine, only minor comments left and then I think it's good to go.
ext/odbc/php_odbc.c
Outdated
if (link->hash) { | ||
zend_hash_del(&ODBCG(non_persistent_connections), link->hash); | ||
} | ||
if (link->hash) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Merge this if's body with the if's body at the bottom of this function.
ext/odbc/php_odbc.c
Outdated
@@ -878,14 +879,14 @@ PHP_FUNCTION(odbc_close_all) | |||
} | |||
|
|||
/* Loop through the non-persistent connection list, now close all non-persistent connections and their results */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment is inaccurate now: previously this looped over non-persistent connections but now it does both non-persistent and persistent.
ext/odbc/php_odbc.c
Outdated
|
||
if (zend_parse_parameters(ZEND_NUM_ARGS(), "O", &pv_conn, odbc_connection_ce) == FAILURE) { | ||
RETURN_THROWS(); | ||
} | ||
|
||
link = Z_ODBC_LINK_P(pv_conn); | ||
CHECK_ODBC_CONNECTION(link->connection); | ||
connection = Z_ODBC_CONNECTION_P(pv_conn); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style nit: I prefer putting the declaration and assignment on the same line, i.e. odbc_connection *connection = Z_ODBC_CONNECTION_P(pv_conn);
. This avoids bugs because it limits the scope of a variable such that it's not accidentally used uninitialized or in places it shouldn't be used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I fixed this in quite some places
ext/odbc/php_odbc.c
Outdated
return ZEND_HASH_APPLY_KEEP; | ||
} | ||
|
||
odbc_connection *list_conn = ((odbc_connection *)(le->ptr)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style nit:
odbc_connection *list_conn = ((odbc_connection *)(le->ptr)); | |
odbc_connection *list_conn = (odbc_connection *) le->ptr; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, this entire function can be simplified into:
static int _close_pconn_with_res(zval *zv, void *p)
{
zend_resource *le = Z_RES_P(zv);
if (le->ptr == p) {
return ZEND_HASH_APPLY_REMOVE;
}
return ZEND_HASH_APPLY_KEEP;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, initially I had a similar code which you suggested, but I wanted to debug it, that's why I created the local variables so that I can inspect them. Now that finally I don't have to do debugging, I'm fine to revert these changes
ext/odbc/php_odbc.c
Outdated
/* If aborted via timer expiration, don't try to call any unixODBC function */ | ||
if (!(PG(connection_status) & PHP_CONNECTION_TIMEOUT)) { | ||
safe_odbc_disconnect(link->connection->hdbc); | ||
SQLFreeConnect(link->connection->hdbc); | ||
SQLFreeEnv(link->connection->henv); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This exact same code (including comment) exists in _close_odbc_pconn
, please factor that out into a common function.
I see this is pre-existing so it's okay to do this separately.
ext/odbc/php_odbc.c
Outdated
res->param_info = NULL; | ||
} | ||
|
||
HashTable *tmp = &res->conn_ptr->results; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: unclear variable name, please use results
or something alike instead.
ext/odbc/php_odbc.c
Outdated
@@ -378,6 +502,12 @@ static PHP_GINIT_FUNCTION(odbc) | |||
ZEND_TSRMLS_CACHE_UPDATE(); | |||
#endif | |||
odbc_globals->num_persistent = 0; | |||
zend_hash_init(&odbc_globals->connections, 0, NULL, NULL, 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style nit:
zend_hash_init(&odbc_globals->connections, 0, NULL, NULL, 1); | |
zend_hash_init(&odbc_globals->connections, 0, NULL, NULL, true); |
} | ||
|
||
static void odbc_connection_free_obj(zend_object *obj) | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style nit: You're inconsistent in {
placement on new line vs end of line. This happens not only here but for other functions too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I saw, {
on its own line is more prevalent in this file, so I'll go with this style.
ext/odbc/php_odbc.c
Outdated
object_init_ex(zv, odbc_connection_ce); | ||
link = Z_ODBC_LINK_P(zv); | ||
link->connection = pecalloc(1, sizeof(odbc_connection), persistent); | ||
zend_hash_init(&link->connection->results, 0, NULL, ZVAL_PTR_DTOR, 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style nit:
zend_hash_init(&link->connection->results, 0, NULL, ZVAL_PTR_DTOR, 1); | |
zend_hash_init(&link->connection->results, 0, NULL, ZVAL_PTR_DTOR, true); |
ext/odbc/php_odbc.c
Outdated
@@ -2190,17 +2299,16 @@ void odbc_do_connect(INTERNAL_FUNCTION_PARAMETERS, int persistent) | |||
persistent = 0; | |||
} | |||
|
|||
char *hashed_details; | |||
int hashed_details_len = spprintf(&hashed_details, 0, "%d_%s_%s_%s_%s_%d", persistent, ODBC_TYPE, db, uid, pwd, cur_opt); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is pre-existing, but this should be size_t instead of int.
@nielsdos I think I managed to fix all your comments. I added the upgrading notes + renamed the namespace according to Tim's RFC which will likely pass. |
@kocsismate arginfo file is outdated, please update and I will review soon :-) |
14aed3f
to
f75d4b3
Compare
Yeah, I noticed it around the same time, fix is pushed :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
3 minor non-critical remarks, LGTM otherwise. Thanks.
ext/odbc/php_odbc.c
Outdated
|
||
zend_object_std_init(&intern->std, class_type); | ||
object_properties_init(&intern->std, class_type); | ||
intern->std.handlers = &odbc_connection_object_handlers; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line isn't necessary I think because the default object handler is set in the ce.
ext/odbc/php_odbc.c
Outdated
{ | ||
int ret; | ||
|
||
ret = SQLDisconnect( handle ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Merge with the declaration at line 123 please.
ext/odbc/php_odbc.c
Outdated
|
||
zend_object_std_init(&intern->std, class_type); | ||
object_properties_init(&intern->std, class_type); | ||
intern->std.handlers = &odbc_result_object_handlers; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same: it's set in the ce so you don't need to set it here
@kocsismate Please check this related failure: https://github.com/php/php-src/actions/runs/8887832558/job/24403758096 |
@iluuu1994 I'm trying to find the root cause of it. Thanks for letting me know about it! |
Update: The failures happen when
|
@kocsismate Data that are persistent but never shared should be marked as GC_MAKE_PERSISTENT_LOCAL. |
@nielsdos Indeed, thank you, I haven't come across this flag yet :( |
I've just filed #14094 to attempt to fix the failures |
As part of php/php-tasks#6 and https://wiki.php.net/rfc/resource_to_object_conversion