mirror of https://github.com/sysown/proxysql
parent
0277b041eb
commit
32dfd0e8e3
@ -0,0 +1,372 @@
|
||||
/**
|
||||
* @file test_mariadb_metadata_check-t.cpp
|
||||
* @brief Tests the column count integrity check for libmariadb.
|
||||
* @details Two different tests are performed:
|
||||
* - Isolated Test: A malformed packet (based on packet that generated the original crash report) is sent by
|
||||
* a fake server to a client. The client should be able to read the packet and continue operations without
|
||||
* presenting memory or internal state issues.
|
||||
* - Integration Test: To exercise this column-count packet check, queries that generates different column
|
||||
* numbers are executed through ProxySQL. Numbers should go below and above '251', to test different
|
||||
* integer values encoding in the 'column-count' packet. See:
|
||||
* https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_dt_integers.html
|
||||
*/
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <stdio.h>
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
#include <utility>
|
||||
|
||||
#include "mysql.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/ioctl.h>
|
||||
|
||||
#include "tap.h"
|
||||
#include "command_line.h"
|
||||
#include "utils.h"
|
||||
|
||||
using std::vector;
|
||||
using std::pair;
|
||||
using std::string;
|
||||
|
||||
/**
|
||||
* @brief MySQL 8.0.39 Greeting message
|
||||
*/
|
||||
unsigned char srv_greeting[] = {
|
||||
// Header
|
||||
0x4a, 0x00, 0x00, 0x00,
|
||||
// Protocol version number
|
||||
0x0a,
|
||||
// Server version string '8.0.39'; and NULL terminator
|
||||
0x38, 0x2e, 0x30, 0x2e, 0x33, 0x39, 0x00,
|
||||
// Length of the server thread ID
|
||||
0x6a, 0x00, 0x00, 0x00,
|
||||
// Salt
|
||||
0x51, 0x04, 0x7d, 0x6f, 0x1a, 0x4b, 0x17, 0x12, 0x00,
|
||||
// Server Capabilities
|
||||
0xff, 0xff,
|
||||
// Server Language: utf8mb4 COLLATE utf8mb4_0900_ai_ci (255))
|
||||
0xff,
|
||||
// Server Status
|
||||
0x02, 0x00,
|
||||
// Extended server capabilities
|
||||
0xff, 0xdf,
|
||||
// Authentication Plugin
|
||||
0x15,
|
||||
// Unused
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Salt
|
||||
0x15, 0x6e, 0x3c, 0x6e, 0x73, 0x0e, 0x6c, 0x5a, 0x28, 0x7d, 0x67, 0x11, 0x00,
|
||||
// 'mysql_native_password'
|
||||
0x6d, 0x79, 0x73, 0x71, 0x6c, 0x5f, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70,
|
||||
0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x00
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief OK packet after accepting fake auth.
|
||||
*/
|
||||
unsigned char srv_login_resp__ok_pkt[] = {
|
||||
0x07, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Malformed packet, invalid encoding of initial 'column-count' packet.
|
||||
*/
|
||||
unsigned char srv_malformed_resultset[] = {
|
||||
// Column-Count 'packet'; invalid encoding; header specifies size '8' for a packet
|
||||
// encoding a single int with value '7', size should be '1'.
|
||||
0x08, 0x00, 0x00, 0x01, 0x07,
|
||||
// No field definition; just value
|
||||
0x35, 0x32, 0x34, 0x32, 0x33, 0x32, 0x32,
|
||||
// EOF
|
||||
0x05, 0x00, 0x00, 0x02, 0xfe, 0x00, 0x00, 0x0a, 0x00,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Valid packet holding the resulset of a 'SELECT 1' query.
|
||||
* @details This is used as a control query to check the client library status after
|
||||
* reading through the whole previously sent 'srv_malformed_resultset'.
|
||||
*/
|
||||
unsigned char srv_resp___select_1[] = {
|
||||
// Column-Count packet
|
||||
0x01, 0x00, 0x00, 0x01, 0x01,
|
||||
// Field definition
|
||||
0x17, 0x00, 0x00, 0x02, 0x03, 0x64, 0x65, 0x66, 0x00, 0x00, 0x00, 0x01, 0x31, 0x00, 0x0c, 0x3f, 0x00,
|
||||
0x02, 0x00, 0x00, 0x00, 0x08, 0x81, 0x00, 0x00, 0x00, 0x00,
|
||||
// Row packet
|
||||
0x02, 0x00, 0x00, 0x03, 0x01, 0x31,
|
||||
// OK packet
|
||||
0x07, 0x00, 0x00, 0x04, 0xfe, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Sequence of messages used by 'fake_server'.
|
||||
* @details The messages fake a simple interation between a client using `libmariadb` and
|
||||
* a MySQL 8.0.39 server. See messages descriptions in list and above.
|
||||
*/
|
||||
const vector<pair<unsigned char*, size_t>> srv_resps = {
|
||||
// Server greeting message
|
||||
{ srv_greeting, sizeof(srv_greeting) },
|
||||
// OK packet after 'Auth' packet from client; auth always works here :)
|
||||
{ srv_login_resp__ok_pkt, sizeof(srv_login_resp__ok_pkt) },
|
||||
// Send malformed resultset; The resultset fields definitions are mangled, and the
|
||||
// 'column-count' packet header purposely fails to encode the payload size.
|
||||
{ srv_malformed_resultset, sizeof(srv_malformed_resultset) },
|
||||
// A simple final resultset corresponding to a 'SELECT 1'. This is used to check if
|
||||
// client is able to read through the whole invalid resulset.
|
||||
{ srv_resp___select_1, sizeof(srv_resp___select_1) }
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Creates a fake server with the specified port.
|
||||
* @details For each client input, the server reads and discards it, and sends the next
|
||||
* message specified in the list 'srv_resps'. For description see doc on 'srv_resps'.
|
||||
* @param port The port in which the server should listen.
|
||||
* @return 0 if the server shutdown succesfully, 1 otherwise.
|
||||
*/
|
||||
int fake_server(int port) {
|
||||
int sockfd, clientfd;
|
||||
struct sockaddr_in server_addr, client_addr;
|
||||
socklen_t client_addr_len = sizeof(client_addr);
|
||||
|
||||
// Create socket
|
||||
sockfd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sockfd == -1) {
|
||||
perror("socket");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Set server address
|
||||
memset(&server_addr, 0, sizeof(server_addr));
|
||||
server_addr.sin_family = AF_INET;
|
||||
server_addr.sin_addr.s_addr = INADDR_ANY;
|
||||
server_addr.sin_port = htons(port);
|
||||
|
||||
int opval = 1;
|
||||
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opval, sizeof(int)) < 0) {
|
||||
perror("setsockopt(SO_REUSEADDR) failed");
|
||||
}
|
||||
|
||||
// Bind socket
|
||||
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
|
||||
perror("bind");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Listen for connections
|
||||
if (listen(sockfd, 5) == -1) {
|
||||
perror("listen");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
diag("Server started on port %d", port);
|
||||
|
||||
// Accept connection
|
||||
clientfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addr_len);
|
||||
if (clientfd == -1) {
|
||||
perror("accept");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
diag(
|
||||
"Client connected addr='%s:%d'",
|
||||
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)
|
||||
);
|
||||
|
||||
struct pollfd pfd;
|
||||
pfd.fd = clientfd;
|
||||
pfd.events = POLLIN;
|
||||
char dummy[256] = { 0 };
|
||||
|
||||
// Receive data
|
||||
for (const auto& resp : srv_resps) {
|
||||
int n = write(clientfd, resp.first, resp.second);
|
||||
diag("Server: Written response n=%d", n);
|
||||
|
||||
if (n < 0) {
|
||||
perror("write");
|
||||
break;
|
||||
}
|
||||
|
||||
if (&resp != &srv_resps.back()) {
|
||||
n = poll(&pfd, 1, -1);
|
||||
if (n < 0) {
|
||||
perror("poll");
|
||||
break;
|
||||
}
|
||||
|
||||
n = recv(clientfd, dummy, sizeof(dummy), 0);
|
||||
diag("Server: Received response n=%d", n);
|
||||
|
||||
if (n == 0) {
|
||||
diag("Client disconnected");
|
||||
break;
|
||||
} else if (n < 0) {
|
||||
perror("recv");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close sockets
|
||||
close(clientfd);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Test the reception of a malformed packet using a fake server.
|
||||
* @details The client performs the following actions:
|
||||
* - 1: Connects to the fake server; connection is always accepted.
|
||||
* - 2: Attempts to read and checks ('ok') the detection the malformed packet.
|
||||
* - 3: Read through the malformed packet. The client keeps attempting to perform a new
|
||||
* query until the remains of the malformed packet are processed by the library, and a
|
||||
* new query can be performed.
|
||||
* - 4: Check ('ok') that a valid resulset is received for final query performed after
|
||||
* receiving the malformed packet.
|
||||
*/
|
||||
void test_malformed_packet() {
|
||||
const uint16_t port { 9091 };
|
||||
const char* user { "foo" };
|
||||
const char* pass { "bar" };
|
||||
std::thread srv_th(fake_server, port);
|
||||
|
||||
MYSQL* conn = mysql_init(NULL);
|
||||
mysql_options(conn, MYSQL_DEFAULT_AUTH, "mysql_native_password");
|
||||
conn->options.client_flag |= CLIENT_DEPRECATE_EOF;
|
||||
|
||||
if (!mysql_real_connect(conn, "127.0.0.1", user, pass, NULL, port, NULL, 0)) {
|
||||
fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(conn));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
{
|
||||
int rc = mysql_query(conn, "SELECT LAST_INSERT_ID()");
|
||||
ok(
|
||||
rc && mysql_errno(conn) == 2027,
|
||||
"'mysql_query' should fail with 'malformed_packet' rc=%d errno=%d error='%s'",
|
||||
rc, mysql_errno(conn), mysql_error(conn)
|
||||
);
|
||||
|
||||
mysql_free_result(mysql_store_result(conn));
|
||||
}
|
||||
|
||||
// Should be able to read through malformed packet to the healthy one
|
||||
{
|
||||
int rc = 0;
|
||||
while ((rc = mysql_query(conn, "SELECT 1"))) {
|
||||
diag(
|
||||
"Client: Still reading malformed packet... rc=%d errno=%d error='%s'",
|
||||
rc, mysql_errno(conn), mysql_error(conn)
|
||||
);
|
||||
}
|
||||
|
||||
diag("Client: Integrity checks allowed to continue reading");
|
||||
|
||||
ok(
|
||||
rc == 0,
|
||||
"Simple query should work rc=%d errno=%d error='%s'",
|
||||
rc, mysql_errno(conn), mysql_error(conn)
|
||||
);
|
||||
|
||||
MYSQL_RES* myres = mysql_store_result(conn);
|
||||
MYSQL_ROW myrow = mysql_fetch_row(myres);
|
||||
|
||||
ok(
|
||||
myres->field_count == 1 && myrow[0][0] == 49,
|
||||
"Fetched resulset should be well-formed fields=%d data=%d",
|
||||
myres->field_count, myrow[0][0]
|
||||
);
|
||||
|
||||
mysql_free_result(myres);
|
||||
}
|
||||
|
||||
cleanup:
|
||||
|
||||
mysql_close(conn);
|
||||
|
||||
pthread_cancel(srv_th.native_handle());
|
||||
srv_th.join();
|
||||
}
|
||||
|
||||
string gen_dyn_cols_select(size_t n) {
|
||||
string q { "SELECT " };
|
||||
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
q += "NULL AS col_" + std::to_string(n);
|
||||
|
||||
if (i < n - 1) {
|
||||
q += ",";
|
||||
}
|
||||
}
|
||||
|
||||
return q;
|
||||
}
|
||||
|
||||
// Needs to be above and below '251'. See:
|
||||
// - https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_dt_integers.html
|
||||
const vector<size_t> cols_counts { 1, 2, 128, 251, 252, 253, 512 };
|
||||
|
||||
/**
|
||||
* @brief Tests that the integrity check introduced in 'libmariadbclient'.
|
||||
* @details Ensures that the check works for queries returning less/more than `251` columns. This forces the
|
||||
* encoding at protocol level of different integers, exercising the check for more values.
|
||||
* @param cl Used for connection creation.
|
||||
*/
|
||||
void test_integrity_check(CommandLine& cl) {
|
||||
MYSQL* conn = mysql_init(NULL);
|
||||
mysql_options(conn, MYSQL_DEFAULT_AUTH, "mysql_native_password");
|
||||
|
||||
if (!mysql_real_connect(conn, cl.host, cl.username, cl.password, NULL, cl.port, NULL, 0)) {
|
||||
fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(conn));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
for (const auto& count : cols_counts) {
|
||||
const string query { gen_dyn_cols_select(count) };
|
||||
int rc = mysql_query(conn, query.c_str());
|
||||
|
||||
if (rc) {
|
||||
diag("Query failed errno=%d error='%s'", mysql_errno(conn), mysql_error(conn));
|
||||
goto cleanup;
|
||||
} else {
|
||||
MYSQL_RES* myres = mysql_store_result(conn);
|
||||
|
||||
ok(
|
||||
myres->field_count == count,
|
||||
"Number of columns should match expected exp=%ld act=%d",
|
||||
count, myres->field_count
|
||||
);
|
||||
|
||||
mysql_free_result(myres);
|
||||
}
|
||||
}
|
||||
|
||||
cleanup:
|
||||
|
||||
mysql_close(conn);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
CommandLine cl;
|
||||
|
||||
if (cl.getEnv()) {
|
||||
diag("Failed to get the required environmental variables.");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
plan(3 + cols_counts.size());
|
||||
|
||||
test_malformed_packet();
|
||||
test_integrity_check(cl);
|
||||
|
||||
cleanup:
|
||||
|
||||
return exit_status();
|
||||
}
|
||||
Loading…
Reference in new issue