From 7b53c2ec3bf3809427055cb4f644ff71712f0812 Mon Sep 17 00:00:00 2001 From: Vladislav Vaintroub Date: Wed, 27 May 2026 16:03:43 +0200 Subject: [PATCH] MDEV-37556 Memory leak in proxy protocol with name resolution enabled When proxy protocol is used and --skip-name-resolve is not set, thd_set_peer_addr() is called twice per connection: once for the real TCP peer (in check_connection), and again for the proxied address (in handle_proxy_header). Each call invokes ip_to_hostname(), which allocates a hostname string (unless loopback connection is used) and stores it as thd->main_security_ctx.host. That code missed to free previously allocated hostname, which results into memory leak. This is now fixed. Also added debug-only test to mysql_client_test, which fakes DNS and IP resolution the same way some perfschema tests do, to emulate remote TCP connection in MTR. --- .../main/mysql_client_test_comp-master.opt | 2 +- .../mysql_client_test_nonblock-master.opt | 2 +- sql/sql_connect.cc | 7 ++ tests/mysql_client_test.c | 79 +++++++++++++++++++ 4 files changed, 88 insertions(+), 2 deletions(-) diff --git a/mysql-test/main/mysql_client_test_comp-master.opt b/mysql-test/main/mysql_client_test_comp-master.opt index ccaa4a8ee6ea8..1f1c9ef787231 100644 --- a/mysql-test/main/mysql_client_test_comp-master.opt +++ b/mysql-test/main/mysql_client_test_comp-master.opt @@ -1,4 +1,4 @@ --loose-enable-performance-schema --max-allowed-packet=32000000 ---proxy-protocol-networks=::1/32,127.0.0.0/8,localhost +--proxy-protocol-networks=::1/32,127.0.0.0/8,localhost,2001:db8::6:6 --sequence=on diff --git a/mysql-test/main/mysql_client_test_nonblock-master.opt b/mysql-test/main/mysql_client_test_nonblock-master.opt index f306d27feac7c..01a77359b03b7 100644 --- a/mysql-test/main/mysql_client_test_nonblock-master.opt +++ b/mysql-test/main/mysql_client_test_nonblock-master.opt @@ -1,4 +1,4 @@ --general-log --general-log-file=$MYSQLTEST_VARDIR/log/master.log --log-output=FILE,TABLE --max-allowed-packet=32000000 ---proxy-protocol-networks=::1,::ffff:127.0.0.1/97,localhost +--proxy-protocol-networks=::1,::ffff:127.0.0.1/97,localhost,2001:db8::6:6 --sequence=on diff --git a/sql/sql_connect.cc b/sql/sql_connect.cc index 0841d589e2f19..26cfbc6edda7c 100644 --- a/sql/sql_connect.cc +++ b/sql/sql_connect.cc @@ -877,6 +877,13 @@ int thd_set_peer_addr(THD *thd, { int rc; + /* Free any previously resolved hostname before overwriting it. */ + if (thd->main_security_ctx.host != my_localhost) + { + my_free((void *) thd->main_security_ctx.host); + thd->main_security_ctx.host= NULL; + } + rc = ip_to_hostname(addr, thd->main_security_ctx.ip, &thd->main_security_ctx.host, diff --git a/tests/mysql_client_test.c b/tests/mysql_client_test.c index d4eacd3e3cc0e..15af8daa9d815 100644 --- a/tests/mysql_client_test.c +++ b/tests/mysql_client_test.c @@ -20747,6 +20747,84 @@ static void test_proxy_header_ignore() } +/* + MDEV-37556 memory leak in proxy protocol for non-loopback + connections. + + Uses debug_dbug variable to set vio_peer_addr_fake_ipv6 etc + to emulate a remote connection +*/ +static void test_proxy_header_dbug_remote_connection() +{ +#ifndef DBUG_OFF + int rc; + MYSQL *m; + MYSQL_RES *result; + MYSQL_ROW row; + v2_proxy_header v2_header; + char addr_bin[16]; + const char *proxy_ipv6 = "2001:db8::6:6"; + int protocol= MYSQL_PROTOCOL_TCP; + + myheader("test_proxy_header_dbug_remote_connection"); + + /* Save and override debug_dbug so name resolution does not hit real DNS. */ + rc= mysql_query(mysql, + "SET @save_debug_dbug = @@GLOBAL.debug_dbug"); + myquery(rc); + rc= mysql_query(mysql, + "SET GLOBAL debug_dbug='+d,vio_peer_addr_fake_ipv6" + ",getnameinfo_fake_ipv6,getaddrinfo_fake_good_ipv6'"); + myquery(rc); + + /* Create a user identified by the hostname that the debug points will produce. */ + rc= mysql_query(mysql, + "CREATE USER 'u'@'santa.claus.ipv6.example.com' IDENTIFIED BY 'password'"); + myquery(rc); + + /* Build a v2 proxy header announcing proxy_ipv6 as the client address. */ + memset(&v2_header, 0, sizeof(v2_header)); + memcpy(v2_header.sig, "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A", 12); + v2_header.ver_cmd = (0x2 << 4) | 0x1; /* PROXY command */ + v2_header.fam = 0x21; /* TCPv6 */ + v2_header.len = htons(36); + inet_pton(AF_INET6, proxy_ipv6, addr_bin); + memcpy(v2_header.addr.ip6.src_addr, addr_bin, 16); + memcpy(v2_header.addr.ip6.dst_addr, addr_bin, 16); + v2_header.addr.ip6.src_port = htons(2222); + v2_header.addr.ip6.dst_port = htons(3306); + + m = mysql_client_init(NULL); + DIE_UNLESS(m != NULL); + mysql_optionsv(m, MARIADB_OPT_PROXY_HEADER, &v2_header, (size_t)52); + mysql_optionsv(m, MYSQL_OPT_PROTOCOL, &protocol); + + if (!mysql_real_connect(m, opt_host, "u", "password", NULL, opt_port, NULL, 0)) + DIE(0); + + /* Verify that the server sees the proxied port, and fake hostname. */ + rc= mysql_query(m, + "SELECT host FROM information_schema.processlist" + " WHERE ID = connection_id()"); + DIE_UNLESS(!rc); + result= mysql_store_result(m); + DIE_UNLESS(result); + row= mysql_fetch_row(result); + DIE_UNLESS(strcmp(row[0], "santa.claus.ipv6.example.com:2222") == 0); + mysql_free_result(result); + + mysql_close(m); + + rc= mysql_query(mysql, "DROP USER 'u'@'santa.claus.ipv6.example.com'"); + myquery(rc); + + /* Restore debug_dbug to whatever it was before this test. */ + rc= mysql_query(mysql, "SET GLOBAL debug_dbug = @save_debug_dbug"); + myquery(rc); +#endif /* !DBUG_OFF */ +} + + static void test_proxy_header() { myheader("test_proxy_header"); @@ -20755,6 +20833,7 @@ static void test_proxy_header() test_proxy_header_tcp("::ffff:192.0.2.1",2222); test_proxy_header_localhost(); test_proxy_header_ignore(); + test_proxy_header_dbug_remote_connection(); }