Skip to content

Commit 469d1f5

Browse files
committed
Add regression test for 'column-count' integrity check in 'libmariadbclient'
1 parent 1793d1a commit 469d1f5

File tree

1 file changed

+372
-0
lines changed

1 file changed

+372
-0
lines changed
Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
/**
2+
* @file test_mariadb_metadata_check-t.cpp
3+
* @brief Tests the column count integrity check for libmariadb.
4+
* @details Two different tests are performed:
5+
* - Isolated Test: A malformed packet (based on packet that generated the original crash report) is sent by
6+
* a fake server to a client. The client should be able to read the packet and continue operations without
7+
* presenting memory or internal state issues.
8+
* - Integration Test: To exercise this column-count packet check, queries that generates different column
9+
* numbers are executed through ProxySQL. Numbers should go below and above '251', to test different
10+
* integer values encoding in the 'column-count' packet. See:
11+
* https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_dt_integers.html
12+
*/
13+
14+
#include <cstring>
15+
#include <vector>
16+
#include <string>
17+
#include <stdio.h>
18+
#include <thread>
19+
#include <unistd.h>
20+
#include <utility>
21+
22+
#include "mysql.h"
23+
24+
#include <fcntl.h>
25+
#include <poll.h>
26+
#include <arpa/inet.h>
27+
#include <sys/socket.h>
28+
#include <sys/ioctl.h>
29+
30+
#include "tap.h"
31+
#include "command_line.h"
32+
#include "utils.h"
33+
34+
using std::vector;
35+
using std::pair;
36+
using std::string;
37+
38+
/**
39+
* @brief MySQL 8.0.39 Greeting message
40+
*/
41+
unsigned char srv_greeting[] = {
42+
// Header
43+
0x4a, 0x00, 0x00, 0x00,
44+
// Protocol version number
45+
0x0a,
46+
// Server version string '8.0.39'; and NULL terminator
47+
0x38, 0x2e, 0x30, 0x2e, 0x33, 0x39, 0x00,
48+
// Length of the server thread ID
49+
0x6a, 0x00, 0x00, 0x00,
50+
// Salt
51+
0x51, 0x04, 0x7d, 0x6f, 0x1a, 0x4b, 0x17, 0x12, 0x00,
52+
// Server Capabilities
53+
0xff, 0xff,
54+
// Server Language: utf8mb4 COLLATE utf8mb4_0900_ai_ci (255))
55+
0xff,
56+
// Server Status
57+
0x02, 0x00,
58+
// Extended server capabilities
59+
0xff, 0xdf,
60+
// Authentication Plugin
61+
0x15,
62+
// Unused
63+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
64+
// Salt
65+
0x15, 0x6e, 0x3c, 0x6e, 0x73, 0x0e, 0x6c, 0x5a, 0x28, 0x7d, 0x67, 0x11, 0x00,
66+
// 'mysql_native_password'
67+
0x6d, 0x79, 0x73, 0x71, 0x6c, 0x5f, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70,
68+
0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x00
69+
};
70+
71+
/**
72+
* @brief OK packet after accepting fake auth.
73+
*/
74+
unsigned char srv_login_resp__ok_pkt[] = {
75+
0x07, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00
76+
};
77+
78+
/**
79+
* @brief Malformed packet, invalid encoding of initial 'column-count' packet.
80+
*/
81+
unsigned char srv_malformed_resultset[] = {
82+
// Column-Count 'packet'; invalid encoding; header specifies size '8' for a packet
83+
// encoding a single int with value '7', size should be '1'.
84+
0x08, 0x00, 0x00, 0x01, 0x07,
85+
// No field definition; just value
86+
0x35, 0x32, 0x34, 0x32, 0x33, 0x32, 0x32,
87+
// EOF
88+
0x05, 0x00, 0x00, 0x02, 0xfe, 0x00, 0x00, 0x0a, 0x00,
89+
};
90+
91+
/**
92+
* @brief Valid packet holding the resulset of a 'SELECT 1' query.
93+
* @details This is used as a control query to check the client library status after
94+
* reading through the whole previously sent 'srv_malformed_resultset'.
95+
*/
96+
unsigned char srv_resp___select_1[] = {
97+
// Column-Count packet
98+
0x01, 0x00, 0x00, 0x01, 0x01,
99+
// Field definition
100+
0x17, 0x00, 0x00, 0x02, 0x03, 0x64, 0x65, 0x66, 0x00, 0x00, 0x00, 0x01, 0x31, 0x00, 0x0c, 0x3f, 0x00,
101+
0x02, 0x00, 0x00, 0x00, 0x08, 0x81, 0x00, 0x00, 0x00, 0x00,
102+
// Row packet
103+
0x02, 0x00, 0x00, 0x03, 0x01, 0x31,
104+
// OK packet
105+
0x07, 0x00, 0x00, 0x04, 0xfe, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00
106+
};
107+
108+
/**
109+
* @brief Sequence of messages used by 'fake_server'.
110+
* @details The messages fake a simple interation between a client using `libmariadb` and
111+
* a MySQL 8.0.39 server. See messages descriptions in list and above.
112+
*/
113+
const vector<pair<unsigned char*, size_t>> srv_resps = {
114+
// Server greeting message
115+
{ srv_greeting, sizeof(srv_greeting) },
116+
// OK packet after 'Auth' packet from client; auth always works here :)
117+
{ srv_login_resp__ok_pkt, sizeof(srv_login_resp__ok_pkt) },
118+
// Send malformed resultset; The resultset fields definitions are mangled, and the
119+
// 'column-count' packet header purposely fails to encode the payload size.
120+
{ srv_malformed_resultset, sizeof(srv_malformed_resultset) },
121+
// A simple final resultset corresponding to a 'SELECT 1'. This is used to check if
122+
// client is able to read through the whole invalid resulset.
123+
{ srv_resp___select_1, sizeof(srv_resp___select_1) }
124+
};
125+
126+
/**
127+
* @brief Creates a fake server with the specified port.
128+
* @details For each client input, the server reads and discards it, and sends the next
129+
* message specified in the list 'srv_resps'. For description see doc on 'srv_resps'.
130+
* @param port The port in which the server should listen.
131+
* @return 0 if the server shutdown succesfully, 1 otherwise.
132+
*/
133+
int fake_server(int port) {
134+
int sockfd, clientfd;
135+
struct sockaddr_in server_addr, client_addr;
136+
socklen_t client_addr_len = sizeof(client_addr);
137+
138+
// Create socket
139+
sockfd = socket(AF_INET, SOCK_STREAM, 0);
140+
if (sockfd == -1) {
141+
perror("socket");
142+
exit(1);
143+
}
144+
145+
// Set server address
146+
memset(&server_addr, 0, sizeof(server_addr));
147+
server_addr.sin_family = AF_INET;
148+
server_addr.sin_addr.s_addr = INADDR_ANY;
149+
server_addr.sin_port = htons(port);
150+
151+
int opval = 1;
152+
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opval, sizeof(int)) < 0) {
153+
perror("setsockopt(SO_REUSEADDR) failed");
154+
}
155+
156+
// Bind socket
157+
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
158+
perror("bind");
159+
exit(1);
160+
}
161+
162+
// Listen for connections
163+
if (listen(sockfd, 5) == -1) {
164+
perror("listen");
165+
exit(1);
166+
}
167+
168+
diag("Server started on port %d", port);
169+
170+
// Accept connection
171+
clientfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addr_len);
172+
if (clientfd == -1) {
173+
perror("accept");
174+
exit(1);
175+
}
176+
177+
diag(
178+
"Client connected addr='%s:%d'",
179+
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)
180+
);
181+
182+
struct pollfd pfd;
183+
pfd.fd = clientfd;
184+
pfd.events = POLLIN;
185+
char dummy[256] = { 0 };
186+
187+
// Receive data
188+
for (const auto& resp : srv_resps) {
189+
int n = write(clientfd, resp.first, resp.second);
190+
diag("Server: Written response n=%d", n);
191+
192+
if (n < 0) {
193+
perror("write");
194+
break;
195+
}
196+
197+
if (&resp != &srv_resps.back()) {
198+
n = poll(&pfd, 1, -1);
199+
if (n < 0) {
200+
perror("poll");
201+
break;
202+
}
203+
204+
n = recv(clientfd, dummy, sizeof(dummy), 0);
205+
diag("Server: Received response n=%d", n);
206+
207+
if (n == 0) {
208+
diag("Client disconnected");
209+
break;
210+
} else if (n < 0) {
211+
perror("recv");
212+
break;
213+
}
214+
}
215+
}
216+
217+
// Close sockets
218+
close(clientfd);
219+
220+
return 0;
221+
}
222+
223+
/**
224+
* @brief Test the reception of a malformed packet using a fake server.
225+
* @details The client performs the following actions:
226+
* - 1: Connects to the fake server; connection is always accepted.
227+
* - 2: Attempts to read and checks ('ok') the detection the malformed packet.
228+
* - 3: Read through the malformed packet. The client keeps attempting to perform a new
229+
* query until the remains of the malformed packet are processed by the library, and a
230+
* new query can be performed.
231+
* - 4: Check ('ok') that a valid resulset is received for final query performed after
232+
* receiving the malformed packet.
233+
*/
234+
void test_malformed_packet() {
235+
const uint16_t port { 9091 };
236+
const char* user { "foo" };
237+
const char* pass { "bar" };
238+
std::thread srv_th(fake_server, port);
239+
240+
MYSQL* conn = mysql_init(NULL);
241+
mysql_options(conn, MYSQL_DEFAULT_AUTH, "mysql_native_password");
242+
conn->options.client_flag |= CLIENT_DEPRECATE_EOF;
243+
244+
if (!mysql_real_connect(conn, "127.0.0.1", user, pass, NULL, port, NULL, 0)) {
245+
fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(conn));
246+
goto cleanup;
247+
}
248+
249+
{
250+
int rc = mysql_query(conn, "SELECT LAST_INSERT_ID()");
251+
ok(
252+
rc && mysql_errno(conn) == 2027,
253+
"'mysql_query' should fail with 'malformed_packet' rc=%d errno=%d error='%s'",
254+
rc, mysql_errno(conn), mysql_error(conn)
255+
);
256+
257+
mysql_free_result(mysql_store_result(conn));
258+
}
259+
260+
// Should be able to read through malformed packet to the healthy one
261+
{
262+
int rc = 0;
263+
while ((rc = mysql_query(conn, "SELECT 1"))) {
264+
diag(
265+
"Client: Still reading malformed packet... rc=%d errno=%d error='%s'",
266+
rc, mysql_errno(conn), mysql_error(conn)
267+
);
268+
}
269+
270+
diag("Client: Integrity checks allowed to continue reading");
271+
272+
ok(
273+
rc == 0,
274+
"Simple query should work rc=%d errno=%d error='%s'",
275+
rc, mysql_errno(conn), mysql_error(conn)
276+
);
277+
278+
MYSQL_RES* myres = mysql_store_result(conn);
279+
MYSQL_ROW myrow = mysql_fetch_row(myres);
280+
281+
ok(
282+
myres->field_count == 1 && myrow[0][0] == 49,
283+
"Fetched resulset should be well-formed fields=%d data=%d",
284+
myres->field_count, myrow[0][0]
285+
);
286+
287+
mysql_free_result(myres);
288+
}
289+
290+
cleanup:
291+
292+
mysql_close(conn);
293+
294+
pthread_cancel(srv_th.native_handle());
295+
srv_th.join();
296+
}
297+
298+
string gen_dyn_cols_select(size_t n) {
299+
string q { "SELECT " };
300+
301+
for (size_t i = 0; i < n; i++) {
302+
q += "NULL AS col_" + std::to_string(n);
303+
304+
if (i < n - 1) {
305+
q += ",";
306+
}
307+
}
308+
309+
return q;
310+
}
311+
312+
// Needs to be above and below '251'. See:
313+
// - https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_dt_integers.html
314+
const vector<size_t> cols_counts { 1, 2, 128, 251, 252, 253, 512 };
315+
316+
/**
317+
* @brief Tests that the integrity check introduced in 'libmariadbclient'.
318+
* @details Ensures that the check works for queries returning less/more than `251` columns. This forces the
319+
* encoding at protocol level of different integers, exercising the check for more values.
320+
* @param cl Used for connection creation.
321+
*/
322+
void test_integrity_check(CommandLine& cl) {
323+
MYSQL* conn = mysql_init(NULL);
324+
mysql_options(conn, MYSQL_DEFAULT_AUTH, "mysql_native_password");
325+
326+
if (!mysql_real_connect(conn, cl.host, cl.username, cl.password, NULL, cl.port, NULL, 0)) {
327+
fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(conn));
328+
goto cleanup;
329+
}
330+
331+
for (const auto& count : cols_counts) {
332+
const string query { gen_dyn_cols_select(count) };
333+
int rc = mysql_query(conn, query.c_str());
334+
335+
if (rc) {
336+
diag("Query failed errno=%d error='%s'", mysql_errno(conn), mysql_error(conn));
337+
goto cleanup;
338+
} else {
339+
MYSQL_RES* myres = mysql_store_result(conn);
340+
341+
ok(
342+
myres->field_count == count,
343+
"Number of columns should match expected exp=%ld act=%d",
344+
count, myres->field_count
345+
);
346+
347+
mysql_free_result(myres);
348+
}
349+
}
350+
351+
cleanup:
352+
353+
mysql_close(conn);
354+
}
355+
356+
int main(int argc, char** argv) {
357+
CommandLine cl;
358+
359+
if (cl.getEnv()) {
360+
diag("Failed to get the required environmental variables.");
361+
return EXIT_FAILURE;
362+
}
363+
364+
plan(3 + cols_counts.size());
365+
366+
test_malformed_packet();
367+
test_integrity_check(cl);
368+
369+
cleanup:
370+
371+
return exit_status();
372+
}

0 commit comments

Comments
 (0)