From 1c49c8db600f7fa99528bbee8559a99be412a7f7 Mon Sep 17 00:00:00 2001 From: "M.Atif Ceylan" Date: Wed, 3 Jun 2026 12:58:32 +0300 Subject: [PATCH 1/3] Fix CLOSE_WAIT socket leak in monitor's close_mysql() when pvio is NULL When a backend server is removed (e.g. an AWS Aurora read replica is deleted) the remote end sends a TCP FIN. The MariaDB connector processes this FIN during the next async I/O operation and clears net.pvio before ProxySQL gets a chance to call close_mysql(). close_mysql() only sends COM_QUIT (which triggers end_server() -> close(fd) inside mysql_close_no_command()) when net.pvio != NULL. With pvio already NULL, mysql_close_no_command() skips end_server() entirely, so the kernel-level TCP socket is never closed. The socket remains in CLOSE_WAIT indefinitely. The monitor threads (ping, read_only, connect) retry every mysql-monitor_ping_interval ms. Each failed retry creates a new connection attempt; when that attempt also fails with pvio=NULL the same path is taken, leaving one more CLOSE_WAIT socket behind. Over time the count grows steadily until proxysql is restarted, at which point all file descriptors are released. Fix: in close_mysql(), add an else-if branch that detects the pvio=NULL / net.fd>0 state and explicitly calls shutdown(SHUT_RDWR) + close(fd) before delegating to mysql_close_no_command(). This mirrors what end_server() would have done and guarantees the kernel socket is released regardless of whether the MariaDB connector has already cleared pvio. Reproducer: 1. Configure ProxySQL with AWS Aurora MySQL (writer + 2 read replicas). 2. Monitor socket count: watch -n5 'grep "^TCP:" /proc/net/sockstat' 3. Delete one read replica from the Aurora console. 4. Observe TCP socket count growing by ~1 per monitor_ping_interval. 5. proxysql restart -> count returns to baseline. After this fix the count stays flat after replica deletion. Related issues: #2908, #3022 --- lib/MySQL_Monitor.cpp | 6 ++++++ lib/mysql_connection.cpp | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/MySQL_Monitor.cpp b/lib/MySQL_Monitor.cpp index 1ea5077175..b9d1f9f844 100644 --- a/lib/MySQL_Monitor.cpp +++ b/lib/MySQL_Monitor.cpp @@ -187,6 +187,8 @@ static int wait_for_mysql(MYSQL *mysql, int status) { static void close_mysql(MYSQL *my) { if (my->net.pvio && !my->options.use_ssl) { + // pvio is valid: send COM_QUIT so the server can cleanly close its side, + // then mysql_close_no_command() will call end_server() -> close(fd). char buff[5]; mysql_hdr myhdr; myhdr.pkt_id=0; @@ -203,6 +205,10 @@ static void close_mysql(MYSQL *my) { #endif fd+=wb; // dummy, to make compiler happy fd-=wb; // dummy, to make compiler happy + } else if (my->net.fd != -1) { + // pvio already cleared; close fd directly to avoid CLOSE_WAIT leak. + close(my->net.fd); + my->net.fd = -1; } mysql_close_no_command(my); } diff --git a/lib/mysql_connection.cpp b/lib/mysql_connection.cpp index a48c36f40e..6a7c916cdc 100644 --- a/lib/mysql_connection.cpp +++ b/lib/mysql_connection.cpp @@ -3019,8 +3019,11 @@ void MySQL_Connection::close_mysql() { #else send(fd, buff, 5, MSG_NOSIGNAL); #endif + } else if (mysql->net.fd != -1 && !mysql->net.pvio) { + // pvio already cleared; close fd directly to avoid CLOSE_WAIT leak. + close(mysql->net.fd); + mysql->net.fd = -1; } -// int rc=0; mysql_close_no_command(mysql); } From 716a13dcfc59304c6b0a9a2b14a8a2ef0cc4605c Mon Sep 17 00:00:00 2001 From: "M.Atif Ceylan" Date: Thu, 2 Jul 2026 18:04:56 +0300 Subject: [PATCH 2/3] Add regression test for CLOSE_WAIT socket leak (PR #5854) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds reg_test_5854_close_wait_leak-t.cpp covering the pvio=NULL / net.fd>0 state that the MariaDB connector reaches when a backend sends a TCP FIN during async I/O. - test_unfixed_path_leaks_fd: mysql_close_no_command() skips end_server() when pvio=NULL, so the fd is never released (fd count stays above baseline) — demonstrates the bug. - test_fixed_path_closes_fd: the explicit close(fd) added in close_mysql() releases the fd (count returns to baseline) — demonstrates the fix. - test_no_close_wait_growth (Linux only): repeated pvio=NULL closes do not accumulate CLOSE_WAIT sockets. Connects directly over TCP (MYSQL_OPT_PROTOCOL) and reads /proc/self/fd and /proc/net/tcp for measurement; fd/CLOSE_WAIT checks are guarded so the test skips cleanly on non-Linux hosts. Related issues: #2908, #3022 --- .../tests/reg_test_5854_close_wait_leak-t.cpp | 231 ++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 test/tap/tests/reg_test_5854_close_wait_leak-t.cpp diff --git a/test/tap/tests/reg_test_5854_close_wait_leak-t.cpp b/test/tap/tests/reg_test_5854_close_wait_leak-t.cpp new file mode 100644 index 0000000000..9063c65cd1 --- /dev/null +++ b/test/tap/tests/reg_test_5854_close_wait_leak-t.cpp @@ -0,0 +1,231 @@ +/** + * @file reg_test_5854_close_wait_leak-t.cpp + * @brief Regression test for CLOSE_WAIT socket leak when net.pvio is NULL. + * @details Verifies two things: + * 1. Without the fix (just mysql_close_no_command when pvio=NULL), the + * socket fd is not released — confirming the bug is real. + * 2. With the fix (explicit close(fd) before mysql_close_no_command), the + * fd is released back — confirming the fix works. + * 3. (Linux only) Repeated fixed closes do not accumulate CLOSE_WAIT sockets. + * + * The pvio=NULL / net.fd>0 state is reproduced by directly zeroing net.pvio + * on a live connection, mirroring what the MariaDB connector does when it + * processes a remote FIN during async I/O. Using shutdown(SHUT_RD)+ping + * to trigger that path is avoided because the connector may also close the + * fd internally during error recovery, which would confound the fd-count + * assertions. + * + * Related: PR #5854, issues #2908 #3022 + */ + +#include "tap.h" +#include "command_line.h" +#include "utils.h" + +#include +#include +#include +#include +#include +#include + +// Returns the number of open fds for this process via /proc/self/fd. +// Returns -1 if not available (non-Linux). +static int count_open_fds() { +#ifdef __linux__ + DIR* dir = opendir("/proc/self/fd"); + if (!dir) return -1; + int n = 0; + struct dirent* e; + while ((e = readdir(dir)) != nullptr) + if (e->d_name[0] != '.') n++; + closedir(dir); + return n - 1; // subtract dirfd opened by opendir itself +#else + return -1; +#endif +} + +#ifdef __linux__ +// Count CLOSE_WAIT entries in /proc/net/tcp and /proc/net/tcp6. +// State 0x08 == CLOSE_WAIT in Linux tcp state encoding. +static int count_close_wait() { + int total = 0; + for (const char* path : { "/proc/net/tcp", "/proc/net/tcp6" }) { + FILE* f = fopen(path, "r"); + if (!f) continue; + char line[512]; + if (!fgets(line, sizeof(line), f)) { fclose(f); continue; } // skip header + while (fgets(line, sizeof(line), f)) { + unsigned int st = 0; + if (sscanf(line, " %*d: %*s %*s %x", &st) == 1 && st == 0x08) + total++; + } + fclose(f); + } + return total; +} +#endif + +// Open a real TCP connection to MySQL and directly zero net.pvio, producing +// the pvio=NULL / net.fd>0 state that the MariaDB connector reaches when it +// processes a remote FIN during async I/O. +static MYSQL* make_pvio_null_conn(const CommandLine& cl) { + MYSQL* my = mysql_init(nullptr); + if (!my) return nullptr; + + unsigned int proto = MYSQL_PROTOCOL_TCP; + mysql_options(my, MYSQL_OPT_PROTOCOL, &proto); + + if (!mysql_real_connect( + my, + cl.mysql_host, cl.mysql_username, cl.mysql_password, + nullptr, cl.mysql_port, nullptr, 0 + )) { + diag("mysql_real_connect failed: %s", mysql_error(my)); + mysql_close(my); + return nullptr; + } + + if (my->net.fd <= 0) { + diag("unexpected: net.fd not set after connect"); + mysql_close(my); + return nullptr; + } + + // Directly replicate the connector's post-EOF field state: + // pvio cleared, fd still open. + my->net.pvio = nullptr; + + return my; +} + +// Test 1: the unfixed path does not release the fd. +// mysql_close_no_command() skips end_server() when pvio=NULL, so net.fd is +// never passed to close(2). We verify this by measuring the open-fd count +// before and after the connection: after the unfixed close the fd must still +// be counted (count stays at "after-connect" level, above baseline). +static void test_unfixed_path_leaks_fd(const CommandLine& cl) { + diag("--- test_unfixed_path_leaks_fd ---"); + + int baseline = count_open_fds(); + if (baseline < 0) { + skip(1, "fd counting not available (/proc/self/fd missing)"); + return; + } + diag("fds baseline (before connect): %d", baseline); + + MYSQL* my = make_pvio_null_conn(cl); + if (!my) { + skip(1, "could not create pvio=NULL connection"); + return; + } + + diag("fds after connect: %d", count_open_fds()); + + // Unfixed path: pvio=NULL, fd not closed explicitly before delegating. + // Mirrors what close_mysql() did before the fix. + mysql_close_no_command(my); + + int after = count_open_fds(); + diag("fds after unfixed close: %d", after); + + ok(after > baseline, + "unfixed path: fd not released — count stays above baseline after " + "mysql_close_no_command on pvio=NULL conn (baseline=%d after=%d)", + baseline, after); +} + +// Test 2: the fixed path releases the fd. +// Explicitly calling close(fd) before mysql_close_no_command() when pvio=NULL +// mirrors the else-if branch added in close_mysql() by PR #5854. +// After the fixed close the fd count must return to baseline. +static void test_fixed_path_closes_fd(const CommandLine& cl) { + diag("--- test_fixed_path_closes_fd ---"); + + int baseline = count_open_fds(); + if (baseline < 0) { + skip(1, "fd counting not available (/proc/self/fd missing)"); + return; + } + // baseline here already includes the leaked fd from test 1 — that is + // fine: we only care that the fixed close does not add another one. + diag("fds baseline (before connect): %d", baseline); + + MYSQL* my = make_pvio_null_conn(cl); + if (!my) { + skip(1, "could not create pvio=NULL connection"); + return; + } + + diag("fds after connect: %d", count_open_fds()); + + // Fixed path: mirrors the else-if branch in close_mysql() (PR #5854). + if (my->net.pvio == nullptr && my->net.fd > 0) { + close(my->net.fd); + my->net.fd = -1; + } + mysql_close_no_command(my); + + int after = count_open_fds(); + diag("fds after fixed close: %d", after); + + ok(after <= baseline, + "fixed path: fd released — count returns to baseline after explicit " + "close(fd)+mysql_close_no_command on pvio=NULL conn (baseline=%d after=%d)", + baseline, after); +} + +// Test 3 (Linux only): repeated fixed closes must not accumulate CLOSE_WAIT sockets. +static void test_no_close_wait_growth(const CommandLine& cl) { + diag("--- test_no_close_wait_growth ---"); + +#ifndef __linux__ + skip(1, "CLOSE_WAIT counting requires /proc/net/tcp (Linux only)"); +#else + const int ITERS = 5; + + int cw_before = count_close_wait(); + diag("CLOSE_WAIT before: %d", cw_before); + + for (int i = 0; i < ITERS; i++) { + MYSQL* my = make_pvio_null_conn(cl); + if (!my) { + diag("iteration %d: could not create pvio=NULL conn", i); + continue; + } + // Fixed path (same as test 2) + if (my->net.pvio == nullptr && my->net.fd > 0) { + close(my->net.fd); + my->net.fd = -1; + } + mysql_close_no_command(my); + usleep(100000); // 100 ms — let kernel reclaim + } + + int cw_after = count_close_wait(); + diag("CLOSE_WAIT after %d iters: %d", ITERS, cw_after); + + // Allow at most 1 slack for unrelated traffic on a busy CI host + ok(cw_after <= cw_before + 1, + "fixed path: CLOSE_WAIT count must not grow after %d pvio=NULL closes " + "(before=%d after=%d)", ITERS, cw_before, cw_after); +#endif +} + +// --------------------------------------------------------------------------- +int main(int argc, const char* argv[]) { + plan(3); + + CommandLine cl; + if (cl.getEnv()) { + diag("Failed to get required env vars"); + return EXIT_FAILURE; + } + + test_unfixed_path_leaks_fd(cl); + test_fixed_path_closes_fd(cl); + test_no_close_wait_growth(cl); + + return exit_status(); +} From bc3dd293d61850556e18494f5b62ed1c4d1c2440 Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Tue, 7 Jul 2026 15:28:41 +0000 Subject: [PATCH 3/3] Harden close_mysql() CLOSE_WAIT fix: correct fd guard, avoid close(0)/double-close MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous CLOSE_WAIT fix guarded the direct close(fd) with `net.fd != -1`, but the bundled MariaDB connector never uses -1 as the invalid value: net.fd is an int, zero-initialized by mysql_init(), and only set to a real socket on a successful connect (ma_net_init -> ma_pvio_get_handle). Consequences: - On a connection that never established (pvio == NULL, net.fd == 0) — exactly the path the monitor takes on a client-side connect error >= 2000, e.g. an Aurora replica being removed — `net.fd != -1` is true and close(net.fd) runs close(0), tearing down the process's stdin. The monitor retries on every ping interval, so this repeats and can corrupt whatever fd later lands in slot 0. - After the connector's own end_server() runs (its reaction to a peer FIN / read error), it has already closed the socket via ma_pvio_close() and set net.pvio = NULL, but leaves net.fd holding the now-stale (possibly reused) fd number (ma_net_end only frees the buffer). Blindly closing it is a double close that can hit an unrelated fd reopened by another thread. Additionally, the monitor variant did not check net.pvio, so it also fired for healthy SSL connections (the first branch requires !use_ssl), raw-closing a live TLS socket and then double-closing it via mysql_close_no_command(). Fix: guard both close_mysql() implementations with `net.pvio == NULL && net.fd > 0 && fcntl(net.fd, F_GETFD) != -1`: - net.pvio == NULL: only when the connector no longer owns the socket; - net.fd > 0: never close(0) on a never-established connection; - fcntl(F_GETFD): skip an already-closed/stale fd (EBADF), avoiding the double close. Both variants now use the identical predicate. Test: reg_test_5854_close_wait_leak-t now exercises the exact production guard via a shared close_mysql_fixed_fixup() helper, and adds a case asserting the guard leaves fd 0 (stdin) untouched on a pvio=NULL / net.fd=0 connection. Registered the test in groups.json so it runs in CI. Claude-Session: https://claude.ai/code/session_01Gq6oqD3FmzUtqc5UnDEz6M --- lib/MySQL_Monitor.cpp | 12 ++- lib/mysql_connection.cpp | 8 +- test/tap/groups/groups.json | 1 + .../tests/reg_test_5854_close_wait_leak-t.cpp | 82 +++++++++++++++---- 4 files changed, 85 insertions(+), 18 deletions(-) diff --git a/lib/MySQL_Monitor.cpp b/lib/MySQL_Monitor.cpp index b9d1f9f844..ece02565c6 100644 --- a/lib/MySQL_Monitor.cpp +++ b/lib/MySQL_Monitor.cpp @@ -205,8 +205,16 @@ static void close_mysql(MYSQL *my) { #endif fd+=wb; // dummy, to make compiler happy fd-=wb; // dummy, to make compiler happy - } else if (my->net.fd != -1) { - // pvio already cleared; close fd directly to avoid CLOSE_WAIT leak. + } else if (my->net.pvio == NULL && my->net.fd > 0 && fcntl(my->net.fd, F_GETFD) != -1) { + // pvio already cleared but the socket fd is still open (e.g. the connector + // processed a peer FIN and dropped pvio without closing the fd): close it + // directly to avoid a CLOSE_WAIT leak. + // The guard is deliberately strict: + // - net.fd is zero-initialized by mysql_init() and only set to a real + // socket on a successful connect, so 'fd > 0' avoids close(0) (which + // would hit stdin) on a connection that never established. + // - fcntl(F_GETFD) rejects an already-closed/stale fd (EBADF), avoiding + // a double close after the connector's own end_server() ran. close(my->net.fd); my->net.fd = -1; } diff --git a/lib/mysql_connection.cpp b/lib/mysql_connection.cpp index 6a7c916cdc..e1db75b3c4 100644 --- a/lib/mysql_connection.cpp +++ b/lib/mysql_connection.cpp @@ -3019,8 +3019,12 @@ void MySQL_Connection::close_mysql() { #else send(fd, buff, 5, MSG_NOSIGNAL); #endif - } else if (mysql->net.fd != -1 && !mysql->net.pvio) { - // pvio already cleared; close fd directly to avoid CLOSE_WAIT leak. + } else if (mysql->net.pvio == NULL && mysql->net.fd > 0 && fcntl(mysql->net.fd, F_GETFD) != -1) { + // pvio already cleared but the socket fd is still open: close it directly + // to avoid a CLOSE_WAIT leak. The guard mirrors the monitor's close_mysql(): + // 'fd > 0' avoids close(0) on a connection that never established (net.fd is + // zero-initialized), and fcntl(F_GETFD) skips an already-closed/stale fd + // (EBADF), avoiding a double close after the connector's own end_server(). close(mysql->net.fd); mysql->net.fd = -1; } diff --git a/test/tap/groups/groups.json b/test/tap/groups/groups.json index 12e531f537..71fe4d0b76 100644 --- a/test/tap/groups/groups.json +++ b/test/tap/groups/groups.json @@ -268,6 +268,7 @@ "reg_test_5639_stmt_execute_max_allowed_packet-t" : [ "mysql84-g6","mysql95-g1" ], "reg_test_5766_libconfig_escape_passthrough-t" : [ "mysql95-g4" ], "reg_test_5790-mariadb_collation_255-t" : [ "mariadb10-galera-g1" ], + "reg_test_5854_close_wait_leak-t" : [ "mysql84-g6","mysql95-g1" ], "reg_test__ssl_client_busy_wait-t" : [ "legacy-g2","mysql-auto_increment_delay_multiplex=0-g2","mysql-multiplexing=false-g2","mysql-query_digests=0-g2","mysql-query_digests_keep_comment=1-g2","mysql84-g2","mysql90-g2","mysql95-g2" ], "reg_test_com_change_user_malformed_packet-t" : [ "mysql84-g6","mysql95-g1" ], "reg_test_compression_split_packets-t" : [ "legacy-g2","mysql-auto_increment_delay_multiplex=0-g2","mysql-multiplexing=false-g2","mysql-query_digests=0-g2","mysql-query_digests_keep_comment=1-g2","mysql84-g2","mysql90-g2","mysql95-g2" ], diff --git a/test/tap/tests/reg_test_5854_close_wait_leak-t.cpp b/test/tap/tests/reg_test_5854_close_wait_leak-t.cpp index 9063c65cd1..d1d638d441 100644 --- a/test/tap/tests/reg_test_5854_close_wait_leak-t.cpp +++ b/test/tap/tests/reg_test_5854_close_wait_leak-t.cpp @@ -1,12 +1,18 @@ /** * @file reg_test_5854_close_wait_leak-t.cpp * @brief Regression test for CLOSE_WAIT socket leak when net.pvio is NULL. - * @details Verifies two things: + * @details Verifies four things: * 1. Without the fix (just mysql_close_no_command when pvio=NULL), the * socket fd is not released — confirming the bug is real. - * 2. With the fix (explicit close(fd) before mysql_close_no_command), the - * fd is released back — confirming the fix works. + * 2. With the fix (close_mysql()'s guarded close(fd) before + * mysql_close_no_command), the fd is released back — confirming it works. * 3. (Linux only) Repeated fixed closes do not accumulate CLOSE_WAIT sockets. + * 4. The guard does NOT close a never-established connection (pvio=NULL, + * net.fd=0), so it never tears down fd 0 (stdin). + * + * Tests 2-4 exercise close_mysql_fixed_fixup(), a verbatim copy of the guard + * added to close_mysql() by PR #5854, so the assertions track the real + * predicate rather than an ad-hoc reimplementation. * * The pvio=NULL / net.fd>0 state is reproduced by directly zeroing net.pvio * on a live connection, mirroring what the MariaDB connector does when it @@ -26,6 +32,7 @@ #include #include #include +#include #include #include @@ -100,6 +107,21 @@ static MYSQL* make_pvio_null_conn(const CommandLine& cl) { return my; } +// Mirror of the production guard added to close_mysql() by PR #5854. +// Kept in one place so the tests exercise the exact predicate the fix uses: +// - only act when pvio is already NULL, +// - fd > 0 so we never close(0) (stdin) on a never-established connection, +// - fcntl(F_GETFD) so we skip an already-closed/stale fd (EBADF). +// Returns true if it actually closed the fd. +static bool close_mysql_fixed_fixup(MYSQL* my) { + if (my->net.pvio == nullptr && my->net.fd > 0 && fcntl(my->net.fd, F_GETFD) != -1) { + close(my->net.fd); + my->net.fd = -1; + return true; + } + return false; +} + // Test 1: the unfixed path does not release the fd. // mysql_close_no_command() skips end_server() when pvio=NULL, so net.fd is // never passed to close(2). We verify this by measuring the open-fd count @@ -160,11 +182,8 @@ static void test_fixed_path_closes_fd(const CommandLine& cl) { diag("fds after connect: %d", count_open_fds()); - // Fixed path: mirrors the else-if branch in close_mysql() (PR #5854). - if (my->net.pvio == nullptr && my->net.fd > 0) { - close(my->net.fd); - my->net.fd = -1; - } + // Fixed path: exercises the exact guard added to close_mysql() (PR #5854). + close_mysql_fixed_fixup(my); mysql_close_no_command(my); int after = count_open_fds(); @@ -194,11 +213,8 @@ static void test_no_close_wait_growth(const CommandLine& cl) { diag("iteration %d: could not create pvio=NULL conn", i); continue; } - // Fixed path (same as test 2) - if (my->net.pvio == nullptr && my->net.fd > 0) { - close(my->net.fd); - my->net.fd = -1; - } + // Fixed path (same guard as test 2) + close_mysql_fixed_fixup(my); mysql_close_no_command(my); usleep(100000); // 100 ms — let kernel reclaim } @@ -213,9 +229,46 @@ static void test_no_close_wait_growth(const CommandLine& cl) { #endif } +// Test 4: the guard must NOT close a never-established connection. +// A MYSQL that failed to connect (or was never connected) has pvio==NULL and +// net.fd==0 (zero-initialized by mysql_init). A naive 'fd != -1' guard would +// call close(0), tearing down the process's stdin. This test verifies the +// production guard leaves fd 0 untouched: stdin must still be a valid fd after +// running the fixup against a pvio==NULL / fd==0 connection. +static void test_guard_protects_fd0(const CommandLine&) { + diag("--- test_guard_protects_fd0 ---"); + + // stdin must be open before we start (it is, under the TAP harness). + bool stdin_ok_before = (fcntl(STDIN_FILENO, F_GETFD) != -1); + if (!stdin_ok_before) { + skip(1, "stdin (fd 0) not open in this environment; cannot test guard"); + return; + } + + MYSQL* my = mysql_init(nullptr); + if (!my) { + skip(1, "mysql_init failed"); + return; + } + // Reproduce the failed-connect field state the monitor hands to close_mysql() + // on a connection error >= 2000: pvio never set, fd still zero-initialized. + my->net.pvio = nullptr; + my->net.fd = 0; + + bool closed = close_mysql_fixed_fixup(my); + mysql_close(my); + + bool stdin_ok_after = (fcntl(STDIN_FILENO, F_GETFD) != -1); + + ok(!closed && stdin_ok_after, + "guard leaves fd 0 alone on a never-established (pvio=NULL, fd=0) conn " + "— stdin still valid (closed=%d stdin_ok=%d)", + closed ? 1 : 0, stdin_ok_after ? 1 : 0); +} + // --------------------------------------------------------------------------- int main(int argc, const char* argv[]) { - plan(3); + plan(4); CommandLine cl; if (cl.getEnv()) { @@ -226,6 +279,7 @@ int main(int argc, const char* argv[]) { test_unfixed_path_leaks_fd(cl); test_fixed_path_closes_fd(cl); test_no_close_wait_growth(cl); + test_guard_protects_fd0(cl); return exit_status(); }