diff --git a/test/tap/tests/repro_3404-mysql_close_fd_leak.cpp b/test/tap/tests/repro_3404-mysql_close_fd_leak.cpp new file mode 100644 index 000000000..83b1e2877 --- /dev/null +++ b/test/tap/tests/repro_3404-mysql_close_fd_leak.cpp @@ -0,0 +1,185 @@ +/** + * @file res_3404-mysql_close_fd_leak.cpp + * @brief This file is not a test to test to be executed, + * it contains an isolated reproduction of issue #3404. + * @details This test contains and isolated reproduction of + * issue #3404. For this, it uses the async 'libmariadbclient' + * API in a fashion in which a file descriptor leak is created. + * + * # Test usage + * + * As the rest of tap tests this file requires serveral constants to + * be supplied, for making use of this file it's required to execute + * the following commands: + * + * ``` + * $ source constants + * $ sudo iptables -A OUTPUT -p tcp --dport $TAP_PORT -j DROP + * ``` + * + * This way we make sure that the connection is going to try to + * be performed and that the MySQL connection attempt is going + * to fail with a timeout. Finally we launch the test file, + * supplying the 'leak' parameter, to instruct the test to + * use the flow that creates the leak: + * + * ``` + * $ ./tests/repro_3404-mysql_close_fd_leak-t leak + * ``` + * + * Check that the file descriptors are being leak: + * + * ``` + * sudo lsof -p $(pgrep -f res_3404-socket_fd_not_closed) | wc -l + * ``` + * + * We can also verify that nothing is being leak if we don't supply + * the 'leak' parameter to the executable. + * + * # Implementation details + * + * For creating the leak this test creates the following flow: + * + * 1. Initialize the 'MYSQL' object using 'mysql_init'. + * 2. Attempt to start a connection with 'mysql_real_connect_start'. + * 3. Connection should timeout due to the server being unreachable. + * Check usage. + * 4. We immediately call 'mysql_close'. Leaking a file descriptor. + * + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "tap.h" +#include "command_line.h" +#include "utils.h" + + +/* Helper function to do the waiting for events on the socket. */ +static int wait_for_mysql(MYSQL *mysql, int status) { + struct pollfd pfd; + int timeout, res; + + pfd.fd = mysql_get_socket(mysql); + pfd.events = + (status & MYSQL_WAIT_READ ? POLLIN : 0) | + (status & MYSQL_WAIT_WRITE ? POLLOUT : 0) | + (status & MYSQL_WAIT_EXCEPT ? POLLPRI : 0); + if (status & MYSQL_WAIT_TIMEOUT) + timeout = 1000*mysql_get_timeout_value(mysql); + else + timeout = -1; + res = poll(&pfd, 1, timeout); + if (res == 0) + return MYSQL_WAIT_TIMEOUT; + else if (res < 0) + return MYSQL_WAIT_TIMEOUT; + else { + int status = 0; + if (pfd.revents & POLLIN) status |= MYSQL_WAIT_READ; + if (pfd.revents & POLLOUT) status |= MYSQL_WAIT_WRITE; + if (pfd.revents & POLLPRI) status |= MYSQL_WAIT_EXCEPT; + return status; + } +} + +int main(int argc, char** argv) { + CommandLine cl; + + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return -1; + } + + bool create_leak = false; + + if (argc == 2) { + std::string param { argv[1] }; + + if (param == "leak") { + create_leak = true; + } + } + + for (int i = 0; i < 1000; i++) { + MYSQL* proxysql = mysql_init(NULL); + MYSQL* ret = NULL; + unsigned int timeout = 1; + + mysql_options(proxysql, MYSQL_OPT_NONBLOCK, 0); + mysql_options(proxysql, MYSQL_OPT_CONNECT_TIMEOUT, (void *)&timeout); + mysql_ssl_set(proxysql, NULL, NULL, NULL, NULL, NULL); + + int status = 0; + + diag("Openning connection number: %d", i); + + status = + mysql_real_connect_start( + &ret, + proxysql, + cl.host, + cl.username, + cl.password, + NULL, + cl.port, + NULL, + CLIENT_SSL + ); + + if (status == 0) { + fprintf( + stderr, "File %s, line %d, Error: %s\n", + __FILE__, __LINE__, mysql_error(proxysql) + ); + return -1; + } + + my_socket sockt = mysql_get_socket(proxysql); + + int state = 0; + MYSQL* ret_mysql = NULL; + + while (status) { + diag(":: Waiting for MySQL server on connection '%d'", i); + status = wait_for_mysql(proxysql, status); + + // don't do finalize the connect! We directly call 'mysql_close' + // creating a leak in the already internally initalized 'fd' + // created by 'libmariadbclient'. + if (status == MYSQL_WAIT_TIMEOUT && create_leak) { + diag(":: Premature close in connection '%d', leaking 'fd'...", i); + break; + } + + // even if we timeout, we call continue + diag(":: Calling 'mysql_real_connect_cont' with status: %d", status); + status = mysql_real_connect_cont(&ret_mysql, proxysql, status); + } + + if (ret_mysql) { + diag(":: This is not expected, make sure the server is not reachable."); + } else { + // now we close + diag( + ":: Calling 'mysql_close' after 'mysql_real_connect_cont' returned status: %d", + status + ); + mysql_close(proxysql); + } + + sleep(1); + } + + return exit_status(); +} +