Skip to content

Commit 42ce2ce

Browse files
authored
Reduce cases where MySQL escaping locks (#2554)
1 parent f4a047d commit 42ce2ce

2 files changed

Lines changed: 118 additions & 3 deletions

File tree

extensions/mysql/mysql/MyDatabase.cpp

Lines changed: 112 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,93 @@ DBType GetOurType(enum_field_types type)
8686
return DBType_Unknown;
8787
}
8888

89+
// Reimplements mysql_cset_escape_slashes() for charsets where no byte of an mb
90+
// sequence can be below 0x80 which makes escaping independent of the connection.
91+
static size_t EscapeStringBackslashes(char *to, const char *from, size_t length)
92+
{
93+
char *start = to;
94+
95+
for (const char *end = from + length; from < end; from++)
96+
{
97+
char esc = '\0';
98+
99+
switch (*from)
100+
{
101+
case 0:
102+
esc = '0';
103+
break;
104+
case '\n':
105+
esc = 'n';
106+
break;
107+
case '\r':
108+
esc = 'r';
109+
break;
110+
case '\\':
111+
case '\'':
112+
case '"':
113+
esc = *from;
114+
break;
115+
case '\032':
116+
esc = 'Z';
117+
break;
118+
}
119+
120+
if (esc)
121+
{
122+
*to++ = '\\';
123+
*to++ = esc;
124+
}
125+
else
126+
{
127+
*to++ = *from;
128+
}
129+
}
130+
131+
*to = '\0';
132+
133+
return (size_t)(to - start);
134+
}
135+
136+
static bool IsByteWiseEscapable(const MY_CHARSET_INFO &cs)
137+
{
138+
if (cs.mbmaxlen <= 1)
139+
{
140+
return true;
141+
}
142+
143+
// big5, gbk, sjis, etc. can end a sequence in 0x5c; UTF-8 can't.
144+
return cs.csname != NULL
145+
&& (strcmp(cs.csname, "utf8") == 0
146+
|| strcmp(cs.csname, "utf8mb3") == 0
147+
|| strcmp(cs.csname, "utf8mb4") == 0);
148+
}
149+
150+
// Caches whether QuoteString() can escape without the connection. Must be called
151+
// with m_FullLock held.
152+
void MyDatabase::RefreshEscapeContext()
153+
{
154+
if (m_bNoBackslashEscapes)
155+
{
156+
return;
157+
}
158+
159+
// Escaping a lone backslash reveals whether the server has NO_BACKSLASH_ESCAPES
160+
// without reading server_status out of the connection struct. Check it, because
161+
// sql_mode can change while a query is in flight, and mishandling here could
162+
// allow for injection.
163+
char probe[3];
164+
if (mysql_real_escape_string(m_mysql, probe, "\\", 1) != 2)
165+
{
166+
m_bNoBackslashEscapes = true;
167+
m_bCanEscapeLocally = false;
168+
return;
169+
}
170+
171+
MY_CHARSET_INFO cs;
172+
mysql_get_character_set_info(m_mysql, &cs);
173+
m_bCanEscapeLocally = IsByteWiseEscapable(cs);
174+
}
175+
89176
MyDatabase::MyDatabase(MYSQL *mysql, const DatabaseInfo *info, bool persistent)
90177
: m_mysql(mysql), m_bPersistent(persistent)
91178
{
@@ -102,6 +189,9 @@ MyDatabase::MyDatabase(MYSQL *mysql, const DatabaseInfo *info, bool persistent)
102189
m_Info.maxTimeout = info->maxTimeout;
103190
m_Info.port = info->port;
104191

192+
// Nothing else can reach the connection yet, so no lock is needed here.
193+
RefreshEscapeContext();
194+
105195
// DBI, for historical reasons, guarantees an initial refcount of 1.
106196
AddRef();
107197
}
@@ -157,8 +247,6 @@ const char *MyDatabase::GetError(int *errCode)
157247

158248
bool MyDatabase::QuoteString(const char *str, char buffer[], size_t maxlength, size_t *newSize)
159249
{
160-
std::lock_guard<std::recursive_mutex> lock(m_FullLock);
161-
162250
unsigned long size = static_cast<unsigned long>(strlen(str));
163251
unsigned long needed = size * 2 + 1;
164252

@@ -171,7 +259,24 @@ bool MyDatabase::QuoteString(const char *str, char buffer[], size_t maxlength, s
171259
return false;
172260
}
173261

174-
needed = mysql_real_escape_string(m_mysql, buffer, str, size);
262+
// Refresh only while the connection is idle; blocking would stall the game
263+
// thread for as long as a threaded query takes to come back.
264+
if (m_FullLock.try_lock())
265+
{
266+
RefreshEscapeContext();
267+
m_FullLock.unlock();
268+
}
269+
270+
if (m_bCanEscapeLocally)
271+
{
272+
needed = static_cast<unsigned long>(EscapeStringBackslashes(buffer, str, size));
273+
}
274+
else
275+
{
276+
std::lock_guard<std::recursive_mutex> lock(m_FullLock);
277+
needed = mysql_real_escape_string(m_mysql, buffer, str, size);
278+
}
279+
175280
if (newSize)
176281
{
177282
*newSize = (size_t)needed;
@@ -313,6 +418,10 @@ bool MyDatabase::SetCharacterSet(const char *characterset)
313418
bool res;
314419
LockForFullAtomicOperation();
315420
res = mysql_set_character_set(m_mysql, characterset) == 0 ? true : false;
421+
if (res)
422+
{
423+
RefreshEscapeContext();
424+
}
316425
UnlockFromFullAtomicOperation();
317426
return res;
318427
}

extensions/mysql/mysql/MyDatabase.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#define _INCLUDE_SM_MYSQL_DATABASE_H_
3434

3535
#include <am-refcounting-threadsafe.h>
36+
#include <atomic>
3637
#include <mutex>
3738
#include "MyDriver.h"
3839

@@ -68,10 +69,15 @@ class MyDatabase
6869
bool SetCharacterSet(const char *characterset);
6970
public:
7071
const DatabaseInfo &GetInfo();
72+
private:
73+
void RefreshEscapeContext();
7174
private:
7275
MYSQL *m_mysql;
7376
std::recursive_mutex m_FullLock;
7477

78+
std::atomic<bool> m_bCanEscapeLocally{false};
79+
bool m_bNoBackslashEscapes = false;
80+
7581
/* ---------- */
7682
DatabaseInfo m_Info;
7783
String m_Host;

0 commit comments

Comments
 (0)