Alan Stott 7848648e09 fix: clean up raw TCP socket on TLS upgrade close (#26766)
## Summary

Fixes #12117, #24118, #25948

When a TCP socket is upgraded to TLS via `tls.connect({ socket })`,
`upgradeTLS()` creates **two** `TLSSocket` structs — a TLS wrapper and a
raw TCP wrapper. Both are `markActive()`'d and `ref()`'d. On close, uws
fires `onClose` through the **TLS context only**, so the TLS wrapper is
properly cleaned up, but the raw TCP wrapper's `onClose` never fires.
Its `has_pending_activity` stays `true` forever and its `ref_count` is
never decremented, **leaking one raw `TLSSocket` per upgrade cycle**.

This affects any code using the `tls.connect({ socket })` "starttls"
pattern:
- **MongoDB Node.js driver** — SDAM heartbeat connections cycle TLS
upgrades every ~10s, causing unbounded memory growth in production
- **mysql2** TLS upgrade path
- Any custom starttls implementation

### The fix

Adds a `defer` block in `NewWrappedHandler(true).onClose` that cleans up
the raw TCP socket when the TLS socket closes:

```zig
defer {
    if (!this.tcp.socket.isDetached()) {
        this.tcp.socket.detach();
        this.tcp.has_pending_activity.store(false, .release);
        this.tcp.deref();
    }
}
```

- **`isDetached()` guard** — skips cleanup if the raw socket was already
closed through another code path (e.g., JS-side `handle.close()`)
- **`socket.detach()`** — marks `InternalSocket` as `.detached` so
`isClosed()` returns `true` safely (the underlying `us_socket_t` is
freed when uws closes the TLS context)
- **`has_pending_activity.store(false)`** — allows JSC GC to collect the
raw socket's JS wrapper
- **`deref()`** — balances the `ref()` from `upgradeTLS`; the remaining
ref is the implicit one from JSC (`ref_count.init() == 1`). When GC
later calls `finalize()` → `deref()`, ref_count hits 0 and `deinit()`
runs the full cleanup chain (markInactive, handlers, poll_ref,
socket_context)

`markInactive()` is intentionally **not** called in the defer — it must
run inside `deinit()` to avoid double-freeing the handlers struct.

### Why Node.js doesn't have this bug

Node.js implements TLS upgrades purely in JavaScript/C++ with OpenSSL,
where the TLS wrapper takes ownership of the underlying socket. There is
no separate "raw socket wrapper" that needs independent cleanup.

## Test Results

### Regression test
```
$ bun test test/js/node/tls/node-tls-upgrade-leak.test.ts
 1 pass, 0 fail
```
Creates 20 TCP→TLS upgrade cycles, closes all connections, runs GC,
asserts `TLSSocket` count stays below 10.

### Existing TLS test suite (all passing)
```
node-tls-upgrade.test.ts      1 pass
node-tls-connect.test.ts     24 pass, 1 skip
node-tls-server.test.ts      21 pass
node-tls-cert.test.ts        25 pass, 3 todo
renegotiation.test.ts          6 pass
```

### MongoDB TLS scenario (patched Bun, 4 minutes)
```
Baseline: RSS=282.4 MB | Heap Used=26.4 MB
Check #4:  RSS=166.7 MB | Heap Used=24.2 MB — No TLSSocket growth. RSS DECREASED.
```

## Test plan

- [x] New regression test passes (`node-tls-upgrade-leak.test.ts`)
- [x] All existing TLS tests pass (upgrade, connect, server, cert,
renegotiation)
- [x] MongoDB TLS scenario shows zero `TLSSocket` accumulation
- [x] Node.js control confirms leak is Bun-specific
- [ ] CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-02-16 23:42:57 -08:00
2026-02-03 22:18:40 -08:00
2025-11-28 17:51:45 +11:00
2026-01-25 10:38:13 -08:00
2026-02-03 22:18:40 -08:00
2025-12-18 18:03:23 -08:00
2026-01-18 00:17:14 -08:00
2025-11-25 11:06:24 -08:00
2026-02-08 01:32:25 -08:00
2026-02-08 01:32:25 -08:00
2025-07-10 00:10:43 -07:00

Logo

Bun

stars Bun speed

Documentation   •   Discord   •   Issues   •   Roadmap

Read the docs →

What is Bun?

Bun is an all-in-one toolkit for JavaScript and TypeScript apps. It ships as a single executable called bun.

At its core is the Bun runtime, a fast JavaScript runtime designed as a drop-in replacement for Node.js. It's written in Zig and powered by JavaScriptCore under the hood, dramatically reducing startup times and memory usage.

bun run index.tsx             # TS and JSX supported out-of-the-box

The bun command-line tool also implements a test runner, script runner, and Node.js-compatible package manager. Instead of 1,000 node_modules for development, you only need bun. Bun's built-in tools are significantly faster than existing options and usable in existing Node.js projects with little to no changes.

bun test                      # run tests
bun run start                 # run the `start` script in `package.json`
bun install <pkg>             # install a package
bunx cowsay 'Hello, world!'   # execute a package

Install

Bun supports Linux (x64 & arm64), macOS (x64 & Apple Silicon) and Windows (x64).

Linux users — Kernel version 5.6 or higher is strongly recommended, but the minimum is 5.1.

x64 users — if you see "illegal instruction" or similar errors, check our CPU requirements

# with install script (recommended)
curl -fsSL https://bun.com/install | bash

# on windows
powershell -c "irm bun.sh/install.ps1 | iex"

# with npm
npm install -g bun

# with Homebrew
brew tap oven-sh/bun
brew install bun

# with Docker
docker pull oven/bun
docker run --rm --init --ulimit memlock=-1:-1 oven/bun

Upgrade

To upgrade to the latest version of Bun, run:

bun upgrade

Bun automatically releases a canary build on every commit to main. To upgrade to the latest canary build, run:

bun upgrade --canary

View canary build

Guides

Contributing

Refer to the Project > Contributing guide to start contributing to Bun.

License

Refer to the Project > License page for information about Bun's licensing.

Description
Bun is a fast, incrementally adoptable all-in-one JavaScript, TypeScript & JSX toolkit. Use individual tools like bun test or bun install in Node.js projects, or adopt the complete stack with a fast JavaScript runtime, bundler, test runner, and package manager built in. Bun aims for 100% Node.js compatibility.
Readme 855 MiB
Languages
Zig 60.6%
C++ 24.8%
TypeScript 8.3%
C 3.3%
JavaScript 1.4%
Other 1.1%