Uh oh!
There was an error while loading. Please reload this page.
Fix GH-13952: sqlite PDO::quote silently corrupts strings with null bytes - #13956
Fix GH-13952: sqlite PDO::quote silently corrupts strings with null bytes#13956ndossche wants to merge 1 commit into
Conversation
…l bytes The built-in way to quote string using sqlite3's custom printf does not support NULL bytes in a string. Reimplement it ourselves. This also gets rid of the integer-based length limit.
| state_in_nulls = true; | ||
| if (source == ZSTR_VAL(unquoted)) { | ||
| quoted_dest--; /* backup initial ' */ | ||
| memcpy(quoted_dest, null_state_enter_at_start, sizeof(null_state_enter_at_start) - 1); |
There was a problem hiding this comment.
would zend_mempcpy be usable here ?
There was a problem hiding this comment.
Yes, I can do that later today
There was a problem hiding this comment.
Looks good to me.
(I think you probably already know this, but I wrote it just in case.)
Although it's completely unnecessary to include it in this PR and is outside of its scope, SQLite3::escapeString has the same issue. The warning in the documentation explains that.
SakiTakamachi
commented
Apr 13, 2024
Also, I have a SQLite community account, would you mind if I report this issue upstream? |
ndossche
commented
Apr 13, 2024
Ugh, right, and we can't really share code between the two extensions afaik. But I dislike having to copy paste code...
You could try, but the API used is a printf-like API which can't really support null-contained strings properly. I guess they would need to make a new API... |
SakiTakamachi
commented
Apr 13, 2024
Yeah, I was thinking about those too. Presumably this is a pdo specific issue and other drivers may have issues with code duplication in the future too. It would be nice to have a place to put code like
Ah, I understand it. Doesn't seem like much worth reporting... |
| -> '' | ||
| x -> 'x' | ||
| \u0000 -> x'00' | ||
| a\u0000b -> 'a'||x'00'||'b' |
There was a problem hiding this comment.
| a\u0000b -> 'a'||x'00'||'b' | |
| a\u0000b ->('a'||x'00'||'b') |
to make sure higher precedence operator (like COLLATE) applies to the whole string - https://www.sqlite.org/lang_expr.html#operators_and_parse_affecting_attributes
| *quoted_dest++ = '\''; | ||
| *quoted_dest = '\0'; | ||
There was a problem hiding this comment.
Not suggesting it's better but alternatively we could convert the entire string to hex if it contains null chars. Pros: faster, simpler impl. Cons (in the all-hex case): more string memory, unreadable return value. Curious what you thought of those trade-offs.
There was a problem hiding this comment.
Yeah the extra memory / IO bandwidth necessary for that case was the main argument against that. In particular, if we don't expect many NULL bytes then the current approach is better.
I think this is a good fit for smart_str. A DOMXPath::quote()-inspired implementation looks something like staticvoid*memchr_inverse(constchar*ptr, constsize_tlen, constcharc)
{
constchar*constend=ptr+len;
while (ptr<end) {
if (*ptr!=c) {
return (void*)ptr;
}
++ptr;
}
returnNULL;
}
/* NB: doesn't handle binary strings... use prepared stmts for that */staticzend_string*sqlite_handle_quoter(pdo_dbh_t*dbh, constzend_string*unquoted, enumpdo_param_typeparamtype)
{
constchar*ptr=ZSTR_VAL(unquoted);
constchar*constend=ZSTR_VAL(unquoted) +ZSTR_LEN(unquoted);
smart_stroutput= {0};
boolis_in_quote= false;
if(ptr==end) {
smart_str_appendl(&output, "''", 2);
returnsmart_str_extract(&output);
}
while (ptr<end) {
// \x00 and ' needs special handlingconstchar*constsingle_quote_ptr=memchr(ptr, '\'', end-ptr);
constchar*constnull_ptr=memchr(ptr, '\0', end-ptr);
constsize_tdistance_to_single_quote=single_quote_ptr ? single_quote_ptr-ptr : end-ptr;
constsize_tdistance_to_null=null_ptr ? null_ptr-ptr : end-ptr;
constsize_tbytes_until_special=MIN(distance_to_single_quote, distance_to_null);
if (bytes_until_special) {
if(!is_in_quote) {
if(ptr!=ZSTR_VAL(unquoted)) {
smart_str_appendl(&output, "||", 2);
}
smart_str_appendc(&output, '\'');
is_in_quote= true;
}
smart_str_appendl(&output, ptr, bytes_until_special);
ptr+=bytes_until_special;
ZEND_ASSERT(ptr <= end);
if(ptr==end) {
break;
}
}
if(*ptr=='\'') {
if(!is_in_quote) {
if(ptr!=ZSTR_VAL(unquoted)) {
smart_str_appendl(&output, "||", 2);
}
smart_str_appendc(&output, '\'');
is_in_quote= true;
}
constchar*constsingle_quotes_end=memchr_inverse(ptr, end-ptr, '\'');
constsize_tnumber_of_consecutive_single_quotes=single_quotes_end ? single_quotes_end-ptr : end-ptr;
smart_str_appendl(&output, ptr, number_of_consecutive_single_quotes);
smart_str_appendl(&output, ptr, number_of_consecutive_single_quotes);
ptr+=number_of_consecutive_single_quotes;
} else {
ZEND_ASSERT(*ptr=='\0');
if(is_in_quote) {
smart_str_appendl(&output, "'||", 3);
is_in_quote= false;
}
constchar*constnull_end=memchr_inverse(ptr, end-ptr, '\0');
constsize_tnumber_of_consecutive_nulls=null_end ? null_end-ptr : end-ptr;
smart_str_appendl(&output, "x'", 2);
for(size_ti=0; i<number_of_consecutive_nulls; ++i) {
smart_str_appendl(&output, "00", 2);
}
smart_str_appendc(&output, '\'');
ptr+=number_of_consecutive_nulls;
}
}
if(is_in_quote) {
smart_str_appendc(&output, '\'');
}
returnsmart_str_extract(&output);
}edit: I thought a smart_str approach would be significantly smaller, but ... it isn't! well i'm surprised 😮 |
@divinity76 I have thought of using smart_str, but it requires more reallocations when the string buffer needs to be extended, which is why I precomputed the length. Ultimately, it's quite difficult to tell which approach is truly better. |
divinity76
commented
Apr 13, 2024
true, smart_str isn't optimally performant, but in this case i think it reduces the possibility of bugs, and is probably fast enough (Maybe some day it will be re-written in memory-safe Rust 🙏 ) |
ndossche
commented
Apr 13, 2024
Might very well be the case. We might try setting up benchmarking although a realistic test might be hard to do 🤷
I wish, although I have a bad feeling that getting the policy RFC for Rust passing in internals will be difficult to do. |
Something is fishy with this approach though, i can reproduce the odd $sqlite3 '' "SELECT LENGTH(x'666f6f00626172');"7$sqlite3 '' "SELECT LENGTH('foo'||x'00'||'bar');"
3$catpuck.c#include<stdio.h>#include<sqlite3.h>intmain()
{
sqlite3*db;
char*err_msg=NULL;
intrc=sqlite3_open(":memory:", &db);
constchar*sql="SELECT LENGTH('foo'||x'00'||'bar');";
sqlite3_stmt*stmt;
rc=sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
rc=sqlite3_step(stmt);
printf("Result: %d\n", sqlite3_column_int(stmt, 0));
sqlite3_finalize(stmt);
sqlite3_close(db);
return0;
}
$gccpuck.c-lsqlite3$ ./a.outResult: 3and I wonder why we can not reproduce the |
divinity76
commented
Apr 13, 2024
umm... this doesn't look safe? |
mvorisek
commented
Apr 13, 2024
This is documented in https://www.sqlite.org/lang_corefunc.html#length
but be aware BLOBs are printed differently - ex. |
ndossche
commented
Apr 13, 2024
What a damn mess, I wonder then if we even should support this given the surprises about the API stability wrt NUL bytes... So weird it works with SELECT and INSERT but there's a different behaviour for LENGTH. |
divinity76
commented
Apr 13, 2024
@nielsdos understandable, thanks for the effort thus far!
Yeah that sounds good. Any objections anyone? |
@nielsdos see https://www.db-fiddle.com/f/8ZSXSyCZyRM4QZR2cptbhC/0 I belive we do not want explicit BLOB cast nor implicit BLOB cast from So this PR LGTM. PS: for string consisting of only NUL bytes the output should be |
For reference, for MySQL CLI (edit) |
PostgreSQL (edit) |
devnexen
commented
Apr 14, 2024
I doubt PostgreSQL supports this feature :) |
SakiTakamachi
commented
Apr 14, 2024
Character concatenation using |
devnexen
commented
Apr 14, 2024
I did not mean concat per say but mixing string and binary. |
Ah, yes it is. However, the following way of writing seems to be supported: In any case, character concatenation in SQLite only supports |
Hmm, when I run it from the cli client, it looks like this... (edit) Ah, maybe the cli just interprets it as a string literal. |
divinity76
commented
Apr 14, 2024
made a x'hex' alternative at #13962 |
If we want to do the |
Implements x'hex' encoding in pdo::quote for handling strings with null bytes, providing a reliable workaround for issue phpGH-13952. An alternative fix is discussed in PR php#13956 PR php#13956 does something interesting, it avoids the overhead of copying to/from sqlite3_snprintf, probably speeding up PDO::quote, but right now I just want to keep things simple. Co-authored-by: Niels Dossche <nielsdos@php.net>
ndossche
commented
Apr 15, 2024
We'll see what Saki's question on the forum will receive as an answer I guess, I can always reopen this. |
resolves issue phpGH-13952, this is basically a smart_str() version of PR php#13956 per php#13962 (comment) this is 1 of the 4 proposed alternatives to the problem, and the pros of this solution is that it produces smaller queries than the alternatives, and retains the sqlite datatype 'string' (instead of changing it to blob), and should make PDO::quote faster as we now avoid the overhead of copying data to/from sqlite3_snprintf. The cons of this solution, that I can think of right now, is that the implementation is non-trivial, involves a bunch of php-allocator-reallocs() (PR php#13956 does not invovle reallocs, as it pre-computes the length. also worth noting that php allocator's reallocs() are faster than libc realloc, often avoiding talking to the OS), and SQLite's LENGTH(('foo'||x'00'||'bar')) returns 3 instead of 7, and binary strings gets the datatype 'string' instead of 'blob' (that can be considered both a pro and a con) Co-authored-by: Niels Dossche <nielsdos@php.net>
SakiTakamachi
commented
Apr 15, 2024
@nielsdos
Indeed, it seems to be working correctly with blobs. |
ndossche
commented
Apr 15, 2024
I guess we shouldn't try to hack around it then? |
SakiTakamachi
commented
Apr 15, 2024
Yes, I agree. In this case, I think it will either convert all strings to binary or raise an error. |
divinity76
commented
Apr 15, 2024
i hope you meant "convert all strings containing null bytes to binary" it would be crazy changing the output of |
yes of course. Only if the string is emebdded with nulls. |
SakiTakamachi
commented
Apr 15, 2024
However, the behavior of returning a binary when a string is passed to
It's unlikely that anyone could look at this description and predict that it would return binary. I still don't have an idea of what is the rational thing to do. |
The built-in way to quote string using sqlite3's custom printf does not support NULL bytes in a string. Reimplement it ourselves. This also gets rid of the integer-based length limit.