From ebe2e9da143197dbee6fa53a73001fec3305e29b Mon Sep 17 00:00:00 2001 From: Meghan Denny Date: Tue, 23 Sep 2025 21:02:34 -0800 Subject: [PATCH] node:net: fix handle leak (#22913) --- scripts/runner.node.mjs | 2 +- src/bun.js/api/bun/socket.zig | 2 +- test/js/node/net/handle-leak.test.ts | 77 ++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 test/js/node/net/handle-leak.test.ts diff --git a/scripts/runner.node.mjs b/scripts/runner.node.mjs index fedfb07c8b..33792ee42c 100755 --- a/scripts/runner.node.mjs +++ b/scripts/runner.node.mjs @@ -80,7 +80,7 @@ function getNodeParallelTestTimeout(testPath) { if (testPath.includes("test-dns")) { return 90_000; } - return 10_000; + return 20_000; } process.on("SIGTRAP", () => { diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 9372b6c1d1..fb18864c97 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -111,7 +111,7 @@ pub fn NewSocket(comptime ssl: bool) type { pub fn doConnect(this: *This, connection: Listener.UnixOrHost) !void { bun.assert(this.socket_context != null); this.ref(); - errdefer this.deref(); + defer this.deref(); switch (connection) { .host => |c| { diff --git a/test/js/node/net/handle-leak.test.ts b/test/js/node/net/handle-leak.test.ts new file mode 100644 index 0000000000..0713995bbd --- /dev/null +++ b/test/js/node/net/handle-leak.test.ts @@ -0,0 +1,77 @@ +import { expect } from "bun:test"; +import { isASAN, isWindows } from "harness"; +import * as net from "node:net"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { setTimeout } from "node:timers/promises"; + +const listen_path = join(tmpdir(), "test-net-successful-connection-handle-leak.sock"); + +const { promise, resolve } = Promise.withResolvers(); +const server = net + .createServer() + .listen(listen_path) + .on("listening", () => resolve()); +await promise; +const address = server.address(); +console.log("server address", address); + +let started; + +started = 0; +while (started < 50_000) { + const promises: Promise[] = []; + for (let i = 0; i < 100; i++) { + const { promise, resolve, reject } = Promise.withResolvers(); + const socket = net + .connect({ path: listen_path }) + .on("connect", () => { + socket.on("close", () => resolve()); + socket.end(); + }) + .on("error", e => { + reject(e); + }); + + promises.push(promise); + started++; + } + await Promise.all(promises); + await setTimeout(1); + console.log(`Completed ${started} connections. RSS: ${(process.memoryUsage.rss() / 1024 / 1024) | 0} MB`); +} + +Bun.gc(true); +const warmup_rss = process.memoryUsage.rss(); + +started = 0; +while (started < 100_000) { + const promises: Promise[] = []; + for (let i = 0; i < 100; i++) { + const { promise, resolve, reject } = Promise.withResolvers(); + const socket = net + .connect({ path: listen_path }) + .on("connect", () => { + socket.on("close", () => resolve()); + socket.end(); + }) + .on("error", e => { + reject(e); + }); + + promises.push(promise); + started++; + } + await Promise.all(promises); + await setTimeout(1); + console.log(`Completed ${started} connections. RSS: ${(process.memoryUsage.rss() / 1024 / 1024) | 0} MB`); +} + +const post_rss = process.memoryUsage.rss(); + +server.close(); + +let margin = 1024 * 1024 * 15; +if (isWindows) margin = 1024 * 1024 * 20; +if (isASAN) margin = 1024 * 1024 * 60; +expect(post_rss - warmup_rss).toBeLessThan(margin);