Skip to content

Fix bug GH-8058 - mysqlnd segfault when prepare fails #8061

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

Closed
wants to merge 3 commits into from

Conversation

kamil-tekiela
Copy link
Member

This is an attempt to fix #8058. We can't set the prepared statement status to INITTED because it's already prepared and has bound variables. We have to keep the error message though when it fails.
However, one existing test case fails because it expects the statement to be in non-executable state. I'd say that expectation is invalid, but I would appreciate second opinion.

@kamil-tekiela kamil-tekiela changed the base branch from master to PHP-8.0 February 8, 2022 12:26
If the prepare fails the statement state should be unaffected
@kamil-tekiela kamil-tekiela marked this pull request as ready for review February 8, 2022 12:34
@cmb69
Copy link
Member

cmb69 commented Feb 8, 2022

An alternative might be to reset the respective result and result_binds:

 ext/mysqlnd/mysqlnd_ps.c | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/ext/mysqlnd/mysqlnd_ps.c b/ext/mysqlnd/mysqlnd_ps.c
index f3dee49d88..3de5c86a6e 100644
--- a/ext/mysqlnd/mysqlnd_ps.c
+++ b/ext/mysqlnd/mysqlnd_ps.c
@@ -504,6 +504,12 @@ fail:
 	if (stmt_to_prepare != stmt && s_to_prepare) {
 		s_to_prepare->m->dtor(s_to_prepare, TRUE);
 	}
+	mysqlnd_stmt_separate_result_bind(s);
+	if (stmt->result) {
+		stmt->result->m.free_result_contents(stmt->result);
+		stmt->result = NULL;
+	}
+	stmt->field_count = 0;
 	stmt->state = MYSQLND_STMT_INITTED;
 
 	DBG_INF("FAIL");

That would let mysqli_stmt_affected_rows.phpt fail with:

002+ [043] Expecting boolean/false, got boolean\1
     mysqli_stmt object is already closed
     done!

The boolean\1 would match libmysqli-client's behavior:

if ($IS_MYSQLND) {
if (false !== ($tmp = mysqli_stmt_store_result($stmt)))
printf("[043] Expecting boolean/false, got %s\%s\n", gettype($tmp), $tmp);
} else {
if (true !== ($tmp = mysqli_stmt_store_result($stmt)))
printf("[043] Libmysql does not care if the previous statement was bogus, expecting boolean/true, got %s\%s\n", gettype($tmp), $tmp);
}

@kamil-tekiela
Copy link
Member Author

@cmb69 Yeah, I thought about resetting the prepared statement, but I don't know if there is an appropriate method for doing this. The way you suggested is not enough as the PS could have already been executed. It would leave the connection in an inconsistent state.

What bothers me is why are we creating a temporary statement if we expect the original one to be reset? Shouldn't we just work with the existing one? There must be a reason, otherwise the code would just work with the existing PS.

I think I need to study this code a little bit more to understand the design.

@kamil-tekiela
Copy link
Member Author

@cmb69 I have removed the temporary prepared statement. What do you think about this? This fulfils the expectation of the PS being non-executable upon failed prepare. I really do not know what the purpose of the temporary PS was. The only reason I can think of was to keep the previous state intact, but then the stmt->state = MYSQLND_STMT_INITTED at the end would do the opposite. Was it done to avoid non-const parameter? I tested it without it and it seems to work as expected. What am I missing?

@cmb69
Copy link
Member

cmb69 commented Feb 9, 2022

I have no idea why there is a temporary PS, either. Maybe it was to better simulate libmysql-client's behavior? I think dropping that temp PS makes sense, but if we want to be conservative, maybe we should only do that for master, and apply your first patch to PHP-8.0/8.1.

@cmb69
Copy link
Member

cmb69 commented Feb 10, 2022

It seems that the Azure pipelines parallelism issue is still unresolved.

@kamil-tekiela
Copy link
Member Author

It seems that the Azure pipelines parallelism issue is still unresolved.

I have restarted it again and it seems to have executed, at least partially

@kamil-tekiela
Copy link
Member Author

if we want to be conservative, maybe we should only do that for master, and apply your first patch to PHP-8.0/8.1.

I thought that this is a great idea, but then I thought about it more. I did some more debugging today and I came to the conclusion that the previous behaviour was buggy. The test I changed had an incorrect expectation and my fix fixed that bug also.

I think we can pull the whole fix into PHP 8.0. It also follows what libmysql does, although I think they reset the PS a little differently. It doesn't seem to be causing any serious adverse effects. It is showing the error message now when prepare fails. Because the order of operations changed, we are now discarding any potentially unfetched result sets; previously there was out of sync error. I doubt anyone would ever complain about this change...

In conclusion, I think that the temporary statement was buggy behaviour and it was not according to spec. The fixed behaviour is correct, fixes the memory leak, and retains the error message from the failed query.

@mdsnins
Copy link
Contributor

mdsnins commented Feb 13, 2022

I just watched this PR :P
Thanks Kamil for cool patch, and I strongly agree that temporal PS is useless and removing it doesn't make any side effects.

Copy link
Member

@cmb69 cmb69 left a comment

Choose a reason for hiding this comment

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

Thank you for the explanation!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

NULL pointer dereference in mysqlnd package (#81706)
3 participants