Skip to content

Fix null SSL* dereference in TLSSocket.getServername after close#30145

Merged
Jarred-Sumner merged 4 commits into
mainfrom
farm/affe1f57/tls-getservername-null-ssl
May 3, 2026
Merged

Fix null SSL* dereference in TLSSocket.getServername after close#30145
Jarred-Sumner merged 4 commits into
mainfrom
farm/affe1f57/tls-getservername-null-ssl

Conversation

@robobun

@robobun robobun commented May 3, 2026

Copy link
Copy Markdown
Collaborator

Repro

const client = await Bun.connect({
  hostname, port, tls: { ... },
  socket: {
    handshake(s) { s.end(); },
    close(s) { s.getServername(); }, // SEGV
  },
});

onClose detaches the socket (this.socket.detach()) before invoking the JS close handler, so this.socket.ssl() returns null there. getServername passed that null straight to SSL_get_servername, which dereferences ssl->hostname → segfault at offset 0x90.

Cause

Every other TLS getter in tls_socket_functions.zig (getCipher, getTLSVersion, getCertificate, getSession, getTLSTicket, …) guards with this.socket.ssl() orelse return .js_undefined. getServername was the one that didn't.

Fix

const ssl_ptr = this.socket.ssl() orelse return .js_undefined;

Verification

Without fix (bun bd, ASAN):

==4116==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000090
    #0 ... std::__uniq_ptr_impl<char, bssl::internal::Deleter>::_M_ptr() const

With fix: test passes, getServername() returns undefined after close.

socket.ssl() returns null once the socket is detached (e.g. inside the
close handler). Every other TLS getter in tls_socket_functions.zig guards
this with `orelse return .js_undefined`, but getServername passed the
null straight to SSL_get_servername, which dereferences ssl->hostname.
@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 900ca1b5-735d-4c43-b0d0-a83e4ffe9741

📥 Commits

Reviewing files that changed from the base of the PR and between a676e74 and 52bdf47.

📒 Files selected for processing (1)
  • test/js/bun/net/socket.test.ts

Walkthrough

Adds a null-check in getServername() to return undefined when the TLS/SSL pointer is absent, and adds a test that ensures calling getServername() on a closed TLS socket returns undefined without crashing.

Changes

Null-Safe Servername Retrieval

Layer / File(s) Summary
Data / Guard
src/bun.js/api/bun/socket/tls_socket_functions.zig
getServername now obtains ssl_ptr via this.socket.ssl() orelse return .js_undefined, returning .js_undefined when the socket has no SSL state.
Behavior / Test
test/js/bun/net/socket.test.ts
New concurrent test getServername on a closed TLS socket should not crash closes the socket during TLS handshake and asserts socket.getServername() resolves to undefined both in the close handler and after client close.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix null SSL* dereference in TLSSocket.getServername after close' directly describes the main change: adding a null check in the getServername function to prevent dereferencing a null SSL pointer.
Description check ✅ Passed The description covers both required sections: 'What does this PR do?' (Repro, Cause, Fix sections) and 'How did you verify your code works?' (Verification section), with clear explanations and code examples.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get your free trial and get 200 agent minutes per Slack user (a $50 value).


Review rate limit: 0/5 reviews remaining, refill in 56 minutes and 16 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the claude label May 3, 2026
@robobun

robobun commented May 3, 2026

Copy link
Copy Markdown
Collaborator Author

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — one-line null guard matching the pattern used by every other TLS getter in this file, with a regression test.

Extended reasoning...

Overview

This PR fixes a segfault in TLSSocket.getServername() when called after the socket has been closed/detached. The fix is a single-line change in src/bun.js/api/bun/socket/tls_socket_functions.zig: const ssl_ptr = this.socket.ssl();const ssl_ptr = this.socket.ssl() orelse return .js_undefined;. A regression test is added to test/js/bun/net/socket.test.ts that connects via TLS, ends the socket in handshake, and calls getServername() from the close handler (where the SSL* is already null).

Security risks

None. This adds a null guard that prevents a null-pointer dereference; it does not change auth, crypto behavior, or expose any new data. Returning undefined after close is consistent with all other TLS getters in this file and is the safe defensive choice.

Level of scrutiny

Low. The change is mechanical and follows an established pattern: I verified that ssl() returns ?*BoringSSL.SSL (src/deps/uws/socket.zig:120, UpgradedDuplex.zig:424, WindowsNamedPipe.zig:543), and that every sibling getter in this file (getCipher, getTLSVersion, getCertificate, getSession, getTLSTicket, getPeerX509Certificate, etc.) already uses the identical orelse return .js_undefined guard. getServername was simply the one outlier that was missed.

Other factors

  • No CODEOWNERS entry covers these paths.
  • The bug-hunting system found no issues.
  • The test is self-contained (loopback TLS listener on port 0, using for cleanup) and asserts undefined is returned both inside the close handler and on the resolved client object afterward.
  • The PR description includes ASAN repro output confirming the crash and verification that the fix resolves it.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/js/bun/net/socket.test.ts`:
- Line 867: The test currently swallows socket errors by defining an empty
error() handler which can leave "await promise" hanging; update the error
handler (the error() function attached to the socket in the test) to reject the
test promise with the received error (e.g., error(err) { reject(err || new
Error('socket error')) }) so any TLS or socket failures cause the awaiting
promise to reject and make failures deterministic and debuggable; ensure the
handler closes the socket if necessary and that the promise variable referenced
by "await promise" is the same promise being rejected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ef3f45ec-a134-4d11-b9e3-3127b6951a97

📥 Commits

Reviewing files that changed from the base of the PR and between 85018b7 and a676e74.

📒 Files selected for processing (2)
  • src/bun.js/api/bun/socket/tls_socket_functions.zig
  • test/js/bun/net/socket.test.ts

Comment thread test/js/bun/net/socket.test.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — one-line null guard matching the existing pattern in this file, with a regression test.

Extended reasoning...

Overview

This PR adds a null check to getServername in src/bun.js/api/bun/socket/tls_socket_functions.zig, changing const ssl_ptr = this.socket.ssl(); to const ssl_ptr = this.socket.ssl() orelse return .js_undefined;. It also adds a regression test in test/js/bun/net/socket.test.ts that calls getServername() from the close handler (after the socket has been detached) and asserts it returns undefined rather than crashing.

Security risks

None. This is purely a defensive null check that prevents a segfault when SSL_get_servername would otherwise be called with a null SSL*. It does not change auth, crypto, or permission semantics — it just returns undefined instead of dereferencing null.

Level of scrutiny

Low. The change is a single-line application of an idiom already used by every sibling function in the same file (getCipher, getTLSVersion, getCertificate, getSession, getTLSTicket, getPeerX509Certificate, etc. all use this.socket.ssl() orelse return ...). I verified that ssl() returns ?*BoringSSL.SSL in src/deps/uws/socket.zig:120 (and the UpgradedDuplex / WindowsNamedPipe variants), so the orelse is type-correct. getServername was simply the one getter that had been missed.

Other factors

  • The PR description includes an ASAN repro confirming the SEGV at offset 0x90 without the fix.
  • The new test is self-contained, uses an ephemeral port and using for cleanup, and follows the style of adjacent tests in the file.
  • No CODEOWNERS entries cover these paths.
  • No bugs were flagged by the bug-hunting system and there are no outstanding reviewer comments.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — one-line null guard matching the pattern used by every other TLS getter in this file.

Extended reasoning...

Overview

This PR changes a single line in src/bun.js/api/bun/socket/tls_socket_functions.zig, adding orelse return .js_undefined to the this.socket.ssl() call in getServername. It also adds a regression test in test/js/bun/net/socket.test.ts that calls getServername() from the close handler (after the underlying SSL* is detached) and asserts it returns undefined instead of segfaulting.

Security risks

None. This is a defensive null check that prevents a segfault. It does not change auth, crypto behavior, or expose any data — it returns undefined where previously the process would crash on a null deref. No new attack surface is introduced.

Level of scrutiny

Low. The fix is purely mechanical: ssl() returns ?*BoringSSL.SSL (verified in src/deps/uws/socket.zig and friends), and literally every other function in this file — getCipher, getTLSVersion, getCertificate, getSession, getTLSTicket, getPeerX509Certificate, getALPNProtocol, etc. — already uses the exact orelse return .js_undefined (or .jsNull()/.false) guard. getServername was the lone outlier. The PR description includes an ASAN repro confirming the segfault and verification that the fix resolves it.

Other factors

  • No CODEOWNERS cover the modified paths.
  • The bug-hunting system found no issues.
  • The one CodeRabbit suggestion (reject the promise on socket error in the test) was addressed in commit 28522bd and the inline comment is resolved.
  • The test follows existing patterns in the file (Promise.withResolvers, using listener, no timers) and won't hang on failure since both error and close settle the promise.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — one-line null guard matching the pattern used by every other TLS getter in this file.

Extended reasoning...

Overview

This PR adds orelse return .js_undefined to getServername in src/bun.js/api/bun/socket/tls_socket_functions.zig, fixing a segfault when getServername() is called after the socket is closed/detached. A regression test in test/js/bun/net/socket.test.ts exercises the crash path.

Security risks

None introduced. This removes a null-pointer dereference (SEGV at offset 0x90 in BoringSSL). Returning undefined when the SSL handle is gone is the same defensive behavior already used by getCipher, getTLSVersion, getCertificate, getSession, getTLSTicket, getPeerX509Certificate, getX509Certificate, getALPNProtocol, renegotiate, etc. in the same file — getServername was the lone holdout.

Level of scrutiny

Low. The native change is a single mechanical line that copies the established idiom verbatim from sibling functions. There is no new control flow, no API surface change, and no ambiguity about the correct return value (undefined is what the function already returns when SSL_get_servername yields null). The test is straightforward and the coderabbit feedback (reject on error) was addressed in 28522bd and marked resolved.

Other factors

The two CI failures reported by robobun (test/bake/dev-and-prod.test.ts on Windows aarch64 and test/js/web/fetch/fetch-http2-client.test.ts on x64-asan) are in unrelated subsystems and not plausibly caused by adding a null check to a TLS getter — they look like pre-existing flakes. No outstanding reviewer comments remain.

@robobun

robobun commented May 3, 2026

Copy link
Copy Markdown
Collaborator Author

CI triage (build 50529): all three failures are pre-existing flakes affecting other branches, unrelated to this one-line null guard.

Test Platform Also failing on
test/bake/dev-and-prod.test.ts win aarch64 50500, 50510, 50417, 50410, 50348
test/js/web/fetch/fetch-http2-client.test.ts debian x64-asan (AtomStringImpl::remove / ThreadLock) 50417, 50400; fix in #29453
test/js/bun/test/parallel/test-http-should-emit-close-when-connection-is-aborted.ts win 2019 x64 / x64-baseline / 11 aarch64 (timeout) 50500, 50510

main at d4cd11c (the commit this branch is merged with) passed as build 50524. test/js/bun/net/socket.test.ts passed on every platform in all three builds of this PR (50350, 50423, 50529).

@Jarred-Sumner Jarred-Sumner merged commit d484fd6 into main May 3, 2026
71 of 77 checks passed
@Jarred-Sumner Jarred-Sumner deleted the farm/affe1f57/tls-getservername-null-ssl branch May 3, 2026 08:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants