* Improve malformed packet test robustness with proper message sequencing

* Fix test assertions to properly validate malformed packet handling
pull/5429/head
Rahim Kanji 2 months ago
parent 4d86e6949c
commit d0ad4b666d

@ -53,6 +53,10 @@ constexpr uint32_t PG_SSL_REQUEST_CODE = 80877103; // (1234 << 16) | 5679
constexpr uint32_t PG_CANCEL_REQUEST_CODE = 80877102; // (1234 << 16) | 5678
constexpr uint32_t PG_GSS_ENCRYPT_CODE = 80877104; // (1234 << 16) | 5680
// Forward declarations for helper functions
bool send_exact(int sock, const void* buf, size_t len);
bool recv_exact(int sock, void* buf, size_t len);
#define REPORT_ERROR_AND_EXIT(fmt, ...) \
do { \
fprintf(stderr, "File %s, line %d: " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__); \
@ -128,8 +132,7 @@ void test_malformed_packet(const std::string& test_name,
}
// Send the malformed data
ssize_t bytes_sent = send(sock, data.data(), data.size(), 0);
if (bytes_sent < 0) {
if (!send_exact(sock, data.data(), data.size())) {
close(sock);
ok(0, "%s: Failed to send data", test_name.c_str());
return;
@ -139,14 +142,17 @@ void test_malformed_packet(const std::string& test_name,
std::vector<char> buffer(BUFFER_SIZE);
ssize_t bytes_received = recv(sock, buffer.data(), buffer.size(), 0);
// Valid outcomes: connection closed OR any response
// The key is that ProxySQL handles the packet without crashing
bool connection_closed = (bytes_received <= 0);
bool got_response = (bytes_received > 0);
bool handled_gracefully = connection_closed || got_response;
// Valid outcomes:
// - Connection closed (bytes_received == 0): ProxySQL rejected/closed connection
// - Timeout (bytes_received < 0 with EAGAIN/EWOULDBLOCK): No response within timeout
// - Error response (bytes_received > 0 && buffer[0] == 'E'): ProxySQL sent error
bool connection_closed = (bytes_received == 0);
bool timeout = (bytes_received < 0 && (errno == EAGAIN || errno == EWOULDBLOCK));
bool got_error_response = (bytes_received > 0 && buffer[0] == 'E');
bool handled_gracefully = connection_closed || timeout || got_error_response;
ok(handled_gracefully, "%s: Malformed packet handled (received: %ld bytes)",
test_name.c_str(), (long)bytes_received);
ok(handled_gracefully, "%s: Malformed packet handled (closed=%d, timeout=%d, error=%d)",
test_name.c_str(), (int)connection_closed, (int)timeout, (int)got_error_response);
close(sock);
}
@ -488,6 +494,21 @@ bool recv_exact(int sock, void* buf, size_t len) {
return true;
}
/**
* @brief Send an exact number of bytes to socket
* @return true if all bytes sent, false on error/short write
*/
bool send_exact(int sock, const void* buf, size_t len) {
const char* ptr = (const char*)buf;
size_t sent = 0;
while (sent < len) {
ssize_t n = send(sock, ptr + sent, len - sent, 0);
if (n <= 0) return false;
sent += n;
}
return true;
}
/**
* @brief Read a PostgreSQL message from socket
*/
@ -537,7 +558,7 @@ void test_malformed_packet_phase2(const std::string& test_name,
// Step 1: Send valid startup packet
auto startup = build_startup_packet({{"user", username}});
if (send(sock, startup.data(), startup.size(), 0) < 0) {
if (!send_exact(sock, startup.data(), startup.size())) {
close(sock);
ok(0, "%s: Failed to send startup", test_name.c_str());
return;
@ -554,7 +575,7 @@ void test_malformed_packet_phase2(const std::string& test_name,
// Step 3: Send password response (cleartext for simplicity)
auto password_pkt = build_password_packet(password);
if (send(sock, password_pkt.data(), password_pkt.size(), 0) < 0) {
if (!send_exact(sock, password_pkt.data(), password_pkt.size())) {
close(sock);
ok(0, "%s: Failed to send password", test_name.c_str());
return;
@ -599,36 +620,79 @@ void test_malformed_packet_phase2(const std::string& test_name,
// Authentication successful - now we have an authenticated connection
// Step 5: Send the malformed packet on the authenticated connection
ssize_t sent = send(sock, malformed_data.data(), malformed_data.size(), 0);
if (sent < 0) {
// Step 5: Drain post-authentication messages until ReadyForQuery ('Z')
// PostgreSQL sends ParameterStatus ('S'), BackendKeyData ('K'), etc. after auth
// We need to consume these before sending the malformed packet to ensure
// the response we read is actually for the malformed packet, not startup noise
bool ready_for_query = false;
for (int i = 0; i < 16; ++i) {
char msg_type = 0;
std::vector<uint8_t> msg_data;
if (!read_message(sock, msg_type, msg_data)) break;
if (msg_type == 'Z') {
ready_for_query = true;
break;
}
if (msg_type == 'E') break; // Error during startup
}
if (!ready_for_query) {
close(sock);
ok(0, "%s: Failed to send malformed data", test_name.c_str());
ok(0, "%s: Did not reach ReadyForQuery before malformed packet", test_name.c_str());
return;
}
// Step 6: Try to receive response
// ProxySQL may send multiple responses (auth completion, parameter status, etc.)
// followed by an error response 'E' for the malformed packet, or close connection
std::vector<char> buffer(BUFFER_SIZE);
ssize_t bytes_received = recv(sock, buffer.data(), buffer.size(), 0);
// Step 6: Send the malformed packet on the authenticated connection
if (!send_exact(sock, malformed_data.data(), malformed_data.size())) {
close(sock);
ok(0, "%s: Failed to send malformed data", test_name.c_str());
return;
}
// Check if we got an error response 'E' or connection closed
// Note: There may be pending post-auth messages, so any of these outcomes is valid:
// 1. Connection closed (ProxySQL rejected the malformed packet)
// 2. 'E' error response (ProxySQL sent an error for the malformed packet)
// 3. Other messages (post-auth protocol messages)
bool connection_closed = (bytes_received <= 0);
bool got_error_response = (bytes_received > 0 && buffer[0] == 'E');
bool got_other_response = (bytes_received > 0 && buffer[0] != 'E');
// Step 7: Wait for response to the malformed packet
// Keep reading messages until we get an error response, connection close, or timeout
bool got_error_response = false;
bool connection_closed = false;
bool timeout = false;
char first_byte = 0;
for (int i = 0; i < 16; ++i) {
char msg_type = 0;
std::vector<uint8_t> msg_data;
// Try to read a message with timeout
ssize_t bytes_received = recv(sock, &msg_type, 1, MSG_PEEK | MSG_DONTWAIT);
if (bytes_received == 0) {
connection_closed = true;
break;
}
if (bytes_received < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
timeout = true;
}
break;
}
// Message available, read it fully
if (!read_message(sock, msg_type, msg_data)) {
connection_closed = true;
break;
}
first_byte = msg_type;
// Check if this is an error response to our malformed packet
if (msg_type == 'E') {
got_error_response = true;
break;
}
// Continue reading if it's other protocol traffic
}
// Any outcome is acceptable as long as ProxySQL doesn't crash
// The key test is the final operational check
bool handled_gracefully = connection_closed || got_error_response || got_other_response;
bool handled_gracefully = connection_closed || timeout || got_error_response;
ok(handled_gracefully, "%s: Phase 2 malformed packet sent (received: %ld bytes, first=0x%02X)",
test_name.c_str(), (long)bytes_received,
bytes_received > 0 ? (unsigned char)buffer[0] : 0);
ok(handled_gracefully, "%s: Phase 2 malformed packet handled (closed=%d, timeout=%d, error=%d, first=0x%02X)",
test_name.c_str(), (int)connection_closed, (int)timeout, (int)got_error_response,
first_byte);
close(sock);
}

Loading…
Cancel
Save