Skip to content
Merged
Show file tree
Hide file tree
Changes from 93 commits
Commits
Show all changes
117 commits
Select commit Hold shift + click to select a range
f6022cc
node:tls: sync the test suite to Node v26.3.0 and fix the gaps it sur…
cirospaciari Jun 29, 2026
b93229c
Merge branch 'main' into claude/node-tls-26-compat
cirospaciari Jul 7, 2026
100afc9
node:tls: give the exported SecureContext constructor its own SSL_CTX…
cirospaciari Jul 7, 2026
cc40e66
node:net: defer abandoned-socket EOF teardown; keep close(hadError) N…
alii Jul 7, 2026
613f6e8
node:tls: constructor authorizationError=null; build _sharedCreds onc…
alii Jul 7, 2026
da98a0d
tls: auto-chain regardless of user ca; map invalid_crl to ERR_CRYPTO_…
alii Jul 7, 2026
88610f8
node:tls,node:net: block prototype-pollution of TLS verification; mat…
cirospaciari Jul 7, 2026
8c24159
tls: apply SSL_VERIFY_NONE per socket on a !requestCert server adopt
cirospaciari Jul 7, 2026
781ffe4
tls: setMaxSendFragment returns false out of OpenSSL's range instead …
cirospaciari Jul 8, 2026
068eb3c
net: only decrement a server's connection count for sockets it counted
cirospaciari Jul 8, 2026
1445535
ci: re-point the tls SSL_CTX LSan suppression at its post-#29932 symbols
cirospaciari Jul 8, 2026
d9dd85a
ci: rebuild — the darwin-26-aarch64 agent pool recovered, previous bu…
cirospaciari Jul 8, 2026
9d11d99
node:net,node:tls: honor onread's false with the true sentinel; fail …
cirospaciari Jul 8, 2026
ee269da
tls: use BoringSSL's handshake-time auto-chain instead of an eager st…
cirospaciari Jul 8, 2026
b122868
tls: pin the mirrored BoringSSL group table with a test and an upgrad…
cirospaciari Jul 8, 2026
2316052
node:net: read() on a paused onread socket still redelivers the decli…
cirospaciari Jul 8, 2026
3d1e593
node:tls: surface a natively-rejected key from the STARTTLS wrap on t…
cirospaciari Jul 8, 2026
3d3c70f
Merge branch 'main' into claude/node-tls-26-compat
cirospaciari Jul 10, 2026
5cb13c4
tls: a bare secureContext server upgrade keeps the context's verify mode
cirospaciari Jul 10, 2026
7c3ede6
Merge branch 'main' into claude/node-tls-26-compat
cirospaciari Jul 10, 2026
74d2a9e
node:tls: address review — shared symbols, native CA parsing, minimal…
cirospaciari Jul 11, 2026
9c12148
tls: release the listener's SSL_CTX at close() and drop the LSan supp…
cirospaciari Jul 11, 2026
15a4c4f
Merge commit 'refs/tmp/mainnow' into ciro/tls2630-merge
cirospaciari Jul 11, 2026
893d8aa
test(node/tls): reword three upstream comment markers in vendored v26…
robobun Jul 11, 2026
0dbe84c
Revert "test(node/tls): reword three upstream comment markers in vend…
robobun Jul 11, 2026
19fc7ae
tls: guard setDefaultCACertificates empty-result on snapshot.length
robobun Jul 11, 2026
ae9a524
test(net): widen mongodb-pattern RSS bound to 16MB on release
robobun Jul 11, 2026
a4dcc15
Merge remote-tracking branch 'origin/main' into claude/node-tls-26-co…
cirospaciari Jul 14, 2026
a3ba65e
http: port the InvalidCRL error to the per-crate error enum
cirospaciari Jul 14, 2026
341a92d
Merge branch 'main' into claude/node-tls-26-compat
cirospaciari Jul 14, 2026
475e825
src: strip PR-added comments not from Node.js source
robobun Jul 15, 2026
f2ef76f
Merge remote-tracking branch 'origin/main' into claude/node-tls-26-co…
robobun Jul 16, 2026
72ccd5c
net: fix ServerHandlers.error TLS detection (this vs data) exposed by…
robobun Jul 16, 2026
1a82a2d
test(node/tls): reword three upstream comment markers in the vendored…
robobun Jul 16, 2026
96081d6
net: ServerHandlers.error TLS branch now destroys instead of no-op de…
robobun Jul 16, 2026
0dfd2dd
Revert "test(node/tls): reword three upstream comment markers in the …
robobun Jul 16, 2026
4c429d6
net: use ErrnoException for the onread zero-length ENOBUFS
robobun Jul 16, 2026
ed5be06
Revert "net: use ErrnoException for the onread zero-length ENOBUFS"
robobun Jul 16, 2026
5bf4147
net: drop stale 'delegation above is a no-op' sentence from the Plain…
robobun Jul 16, 2026
1eeea8e
net: SocketEmitEndNT delegates to finishSocketEnd for the EOF body
robobun Jul 16, 2026
b65ea59
Merge branch 'main' into claude/node-tls-26-compat
cirospaciari Jul 16, 2026
00b8c5f
Merge branch 'main' into claude/node-tls-26-compat
cirospaciari Jul 17, 2026
0fc6370
test(net): widen handle-leak RSS margin to 24MB on release
robobun Jul 17, 2026
d7139f8
Merge remote-tracking branch 'origin/main' into claude/node-tls-26-co…
robobun Jul 17, 2026
37e5363
test(tls): bind ssl-ctx-cache servers to 127.0.0.1 explicitly
robobun Jul 17, 2026
f7881db
Merge branch 'main' into claude/node-tls-26-compat
cirospaciari Jul 17, 2026
e4dc6ba
test(net,tls): bind PR-added servers and clients to 127.0.0.1 explicitly
robobun Jul 17, 2026
d4b92a1
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 17, 2026
5856176
test(net): add rationale for the handle-leak RSS margin and fix stale…
robobun Jul 17, 2026
44134b9
test(net): widen connect({path}) reused-handle page-count threshold t…
robobun Jul 17, 2026
8f2235e
tls: vendor test-tls-delayed-attach-error from Node v26.3.0
cirospaciari Jul 18, 2026
d6e2b68
tls: stage duplex bytes that arrive before the TLS engine starts
cirospaciari Jul 18, 2026
4a8b668
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 18, 2026
4fca545
tls: honour requestCert on the duplex path, deliver a pre-engine EOF
cirospaciari Jul 18, 2026
562bfb8
tls: SNICallback on adopted sockets, server-side ALPN on the adopt path
cirospaciari Jul 20, 2026
2c898a8
test: quarantine worker-terminate ASAN crashes, matching main
cirospaciari Jul 20, 2026
f413d61
test: quarantine musl-only onGC timing flakes, matching #34631
cirospaciari Jul 20, 2026
6c35398
test: mark no-orphans flaky on macOS
cirospaciari Jul 20, 2026
e60fd5a
test: quarantine http2 reset-flood ASAN crash
cirospaciari Jul 21, 2026
c4cc821
tls: send close_notify on end() and flush it on the duplex path (+3 t…
cirospaciari Jul 21, 2026
a7d1efe
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 21, 2026
8c733fe
Remove scratch folder
Jarred-Sumner Jul 21, 2026
b3dbbcf
test: drop expectations.txt changes from this branch
Jarred-Sumner Jul 21, 2026
16468e6
test: sync expectations.txt to current main
cirospaciari Jul 21, 2026
dc2078b
jsc: drain queued JIT plans before an explicit synchronous GC
cirospaciari Jul 21, 2026
d78bc17
ci: allow binary size growth for the node-v26 compat stack [allow size]
cirospaciari Jul 21, 2026
c4f0dc5
jsc: sanitize the stack before an explicit synchronous GC
cirospaciari Jul 21, 2026
605c98b
ci: keep the binary size allowance on the stack tip [allow size]
cirospaciari Jul 21, 2026
8426d12
event loop: scrub callee stack before timer and immediate dispatch
cirospaciari Jul 21, 2026
e838710
event loop: add the missing safety comment on the scrub memset [allow…
cirospaciari Jul 21, 2026
1c05b51
ci: keep the binary size allowance on the stack tip [allow size]
cirospaciari Jul 21, 2026
273f2e7
timers: keep safety comments adjacent to their unsafe blocks [allow s…
cirospaciari Jul 21, 2026
1991dda
ci: keep the binary size allowance on the stack tip [allow size]
cirospaciari Jul 21, 2026
7233c65
test harness: expose gc() with Node's synchronous semantics
cirospaciari Jul 22, 2026
89830b3
ci: keep the binary size allowance on the stack tip [allow size]
cirospaciari Jul 22, 2026
6b3857d
event loop: widen the callee-stack scrub window to 32 KB [allow size]
cirospaciari Jul 22, 2026
ab02c18
timers: scrub the callee stack inside drain_timers, covering all plat…
cirospaciari Jul 22, 2026
9a26b36
ci: keep the binary size allowance on the stack tip [allow size]
cirospaciari Jul 22, 2026
909eb69
Remove the explicit-GC stack-scrub and JIT-drain experiments
cirospaciari Jul 22, 2026
562ba7d
tls: move the duplex server-verify override into SSLWrapper
cirospaciari Jul 22, 2026
42ee0fb
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 22, 2026
8af2ae5
ci: keep the binary size allowance on the stack tip [allow size]
cirospaciari Jul 22, 2026
fd590d4
Address review findings: TLS spill-close hang, buffer and guard cleanups
cirospaciari Jul 22, 2026
3555483
tls: fix injected/wrapped-socket upgrades (double TLS, pre-wrap write…
cirospaciari Jul 22, 2026
56f26c7
lint: hoist the handle read above the destroy conditional chain [allo…
cirospaciari Jul 22, 2026
d13336c
net: hoist the adopted-TLS-pair close deferral into named NT helpers
cirospaciari Jul 23, 2026
6e1d618
test: pin the rejected-handshake socket semantics to node v26.3.0
cirospaciari Jul 23, 2026
652900e
Merge branch 'main' into claude/tls-delayed-attach-error
robobun Jul 23, 2026
61c0c4b
Address review: scope abandoned-socket teardown to server sockets; gu…
robobun Jul 23, 2026
9f069ae
tls: fail closed on inline-rejected handshakes
cirospaciari Jul 23, 2026
7404446
test: accumulate chunks before asserting in the late-reader client test
robobun Jul 23, 2026
7b7f602
Merge main into claude/tls-delayed-attach-error
cirospaciari Jul 23, 2026
ff85f01
ci: keep the binary size allowance on the stack tip [allow size]
cirospaciari Jul 23, 2026
445f997
net: emit kUpgradeAttached on the deferred-adoption duplex fallback
robobun Jul 23, 2026
1ee64e0
net: use reportError for a throwing secureConnection listener
robobun Jul 23, 2026
c99b5e7
tls: route handshake-callback throws to uncaughtException, drop the J…
robobun Jul 23, 2026
0c8282e
net: re-run the kReaderInterest check after secureConnection for TLS …
robobun Jul 23, 2026
f87c692
tls: keep call_error_handler for Bun-native; scope the uncaughtExcept…
robobun Jul 23, 2026
464d953
test: revert expectations.txt additions
robobun Jul 23, 2026
3c1508e
tls: report throwing 'secure'/'secureConnect' listeners on the client…
cirospaciari Jul 23, 2026
b69b20b
node:tls: wire ecdhCurve through to SSL_CTX_set1_groups_list
robobun Jul 23, 2026
0ab9c3c
http: make create_bun_socket_error_t match exhaustive in HTTPContext
robobun Jul 23, 2026
c49a526
uws: use BUFFER_SIZE for the close_notify drain; trim comments to <=3…
robobun Jul 23, 2026
a040f5b
ci: retrigger
robobun Jul 23, 2026
5287b6c
test: use module-scope harness imports in the new handshake-throw tests
robobun Jul 23, 2026
c2c02b4
test: also handshake P-384 against an ecdhCurve:'auto' server
robobun Jul 23, 2026
1e87c93
openssl: leave BoringSSL error on queue when ecdh_curve rejects
robobun Jul 23, 2026
29701d3
test: move ecdhCurve coverage to its own file
robobun Jul 23, 2026
55b277b
node:tls: read tls.DEFAULT_ECDH_CURVE as the fallback for omitted ecd…
robobun Jul 23, 2026
d15621a
Merge remote-tracking branch 'origin/farm/782bad99/tls-ecdh-curve' in…
robobun Jul 23, 2026
7b0fcbc
test: createServer rejects an unknown ecdhCurve synchronously, like Node
robobun Jul 23, 2026
adc5aae
tls: match node's user-visible secure/secureConnect contract and _tls…
cirospaciari Jul 23, 2026
8bcd86e
tls: fail closed on every failed handshake flavor, not just policy re…
cirospaciari Jul 23, 2026
859335b
Merge branch 'main' into claude/tls-delayed-attach-error
cirospaciari Jul 23, 2026
b9ca36f
Merge branch 'main' into claude/tls-delayed-attach-error
cirospaciari Jul 23, 2026
a808a55
net: extract onClientHandshakeComplete; note why the ecdhCurve error …
robobun Jul 23, 2026
99609ea
Merge branch 'main' into claude/tls-delayed-attach-error
cirospaciari Jul 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/commands/upgrade-boringssl.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ In the bun repo:

- `scripts/build/deps/boringssl.ts` — set `BORINGSSL_COMMIT` to `$NEW_SHA`.
- `test/js/node/process/process.test.js` — update the `boringssl:` entry in `expectedVersions` to `$NEW_SHA`.
- `src/js/node/tls.ts` — two hand-maintained mirrors of BoringSSL tables must be re-derived from the new pin (a test pins the current set, but cannot see upstream additions on its own): `_VALID_CIPHERS_SET` ← `ssl/ssl_cipher.cc` `kCiphers`, `CIPHER_LIST_SELECTORS` ← `ssl/ssl_cipher.cc` `kCipherAliases`.
- `src/js/node/tls.ts` — three hand-maintained mirrors of BoringSSL tables must be re-derived from the new pin (a test pins the current set, but cannot see upstream additions on its own): `SUPPORTED_ECDH_GROUPS` ← `ssl/ssl_key_share.cc` `kNamedGroups` (every `name` and non-empty `alias`), `_VALID_CIPHERS_SET` ← `ssl/ssl_cipher.cc` `kCiphers`, `CIPHER_LIST_SELECTORS` ← `ssl/ssl_cipher.cc` `kCipherAliases`.
- Regenerate the source lists (the file's header comment has the exact one-liner). Only `gen/sources.json` is authoritative — diff old vs new and apply the delta:

```sh
Expand Down
251 changes: 208 additions & 43 deletions packages/bun-usockets/src/crypto/openssl.c

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions packages/bun-usockets/src/libusockets.h
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,12 @@ void us_listen_socket_on_server_name(struct us_listen_socket_t *ls,
* after the socket closed (no-op). */
void us_socket_sni_resolve(us_socket_r s, struct ssl_ctx_st *ctx, int error);
void *us_socket_server_name_userdata(us_socket_r s);
/* Socket-level SNI resolver, for a server-side socket adopted into TLS with no
* listen socket behind it. Same contract as the listener resolver: an owned
* SSL_CTX ref or NULL; *abort_handshake 1 = drop silently, 2 = suspend. */
typedef struct ssl_ctx_st *(*us_socket_server_name_cb)(struct us_socket_t *socket,
const char *hostname, int *abort_handshake);
void us_socket_on_server_name(us_socket_r s, us_socket_server_name_cb cb);

/* ── Connect ──────────────────────────────────────────────────────────────
* Returns either us_socket_t* (fast path, *is_connecting=1) or
Expand Down
4 changes: 4 additions & 0 deletions src/js/internal/test/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ function internalBinding(name: string) {
return { UDP: require("internal/dgram").UDP };
case "tcp_wrap":
return { TCP: TestTCPWrap, constants: { SOCKET: 0, SERVER: 1 } };
case "cares_wrap":
// Only the pure IP-normalizer the vendored tls/dns tests reach for; the
// resolver surface lives in node:dns.
return { canonicalizeIP: require("bun:internal-for-testing").canonicalizeIP };
default:
throw new Error(`internalBinding("${name}") is not implemented in Bun`);
}
Expand Down
144 changes: 114 additions & 30 deletions src/js/node/net.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,12 @@ const kAttach = Symbol("kAttach");
const kCloseRawConnection = Symbol("kCloseRawConnection");
const kpendingRead = Symbol("kpendingRead");
const kupgraded = Symbol("kupgraded");
const kAdoptedTLSRaw = Symbol("kAdoptedTLSRaw");
const ksocket = Symbol("ksocket");
const khandlers = Symbol("khandlers");
const kclosed = Symbol("closed");
const kended = Symbol("ended");
const kReaderInterest = Symbol("kReaderInterest");
const kpendingSession = Symbol("pendingSession");
const kSNIError = Symbol("kSNIError");
const kALPNError = Symbol("kALPNError");
Expand Down Expand Up @@ -237,6 +239,16 @@ function endNT(socket, callback, err) {
function emitCloseNT(self, hasError) {
self.emit("close", hasError);
}
// Shared-fd TLS pair teardown: mirrors node's close ordering, where the
// close-callbacks phase runs after the check phase (lib/net.js close path in
// node v26.3.0), so destroy()-time setImmediates still see the pair alive.
function closeAdoptedTLSRawNT(handle, self, isException) {
setImmediate(closeAdoptedTLSRawNowNT, handle, self, isException);
}
function closeAdoptedTLSRawNowNT(handle, self, isException) {
handle.close(onSocketHandleClosed);
setImmediate(emitCloseNT, self, isException);
}
function detachSocket(self) {
if (!self) self = this;
self._handle = null;
Expand Down Expand Up @@ -628,6 +640,9 @@ function SocketEmitEndNT(self, _err?) {
}
if (!self[kended]) {
finishSocketEnd(self);
if (!self.allowHalfOpen && self[kReaderInterest] === false) {
setImmediate(destroyAbandonedNT, self);
}
Comment thread
cirospaciari marked this conversation as resolved.
} else if (_err && !self.destroyed) {
// An error excluded from the synthesis above (teardown noise, or no
// listener attached): nothing more is coming, but the socket still has to
Expand Down Expand Up @@ -936,31 +951,39 @@ const ServerHandlers: SocketHandler<NetSocket> = {
if (pauseOnConnect) {
self.pause();
}
if (server) {
// onServerSocketSecure hands the socket to the user before emitting
// 'secureConnection': from here on errors reach the user's 'error'
// listener instead of the server's 'tlsClientError'. A socket destroyed
// mid-handshake, or one whose control was already released by an earlier
// completion, is dropped here.
// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/tls/wrap.js#L1227
if (!self.destroyed && self._releaseControl()) {
const connectionListener = server[bunSocketServerOptions]?.connectionListener;
if (typeof connectionListener === "function") {
server.prependOnceListener("secureConnection", connectionListener);
try {
if (server) {
// onServerSocketSecure hands the socket to the user before emitting
// 'secureConnection': from here on errors reach the user's 'error'
// listener instead of the server's 'tlsClientError'. A socket destroyed
// mid-handshake, or one whose control was already released by an earlier
// completion, is dropped here.
// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/tls/wrap.js#L1227
if (!self.destroyed && self._releaseControl()) {
const connectionListener = server[bunSocketServerOptions]?.connectionListener;
if (typeof connectionListener === "function") {
server.prependOnceListener("secureConnection", connectionListener);
}
server.emit("secureConnection", self);
}
server.emit("secureConnection", self);
}
}
if (self.destroyed) return;
// after secureConnection event we emmit secure and secureConnect
self.emit("secure", self);
self.emit("secureConnect", verifyError);
if (!pauseOnConnect && !self.destroyed) {
// Node's server-side TLSSocket is manualStart: initRead() only read(0)s
// the handle, leaving readableFlowing null, so bytes that arrive before
// a 'data' listener attaches are buffered instead of dropped.
// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/tls/wrap.js#L502-L524
self.read(0);
if (self.destroyed) return;
// after secureConnection event we emmit secure and secureConnect
self.emit("secure", self);
self.emit("secureConnect", verifyError);
} catch (err) {
// A throwing listener is a programmer error, not a TLS failure: Node
// crashes with an uncaught exception instead of feeding the socket error
// path (node v26.3.0 lib/_tls_wrap.js; test-tls-handshake-exception).
process.nextTick(rethrowUncaught, err);
Comment thread
cirospaciari marked this conversation as resolved.
Outdated
} finally {
if (!pauseOnConnect && !self.destroyed) {
// Node's server-side TLSSocket is manualStart: initRead() only read(0)s
// the handle, leaving readableFlowing null, so bytes that arrive before
// a 'data' listener attaches are buffered instead of dropped.
// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/tls/wrap.js#L502-L524
self.read(0);
}
}
},
error(socket, error) {
Expand Down Expand Up @@ -1028,6 +1051,10 @@ const ServerHandlers: SocketHandler<NetSocket> = {
binaryType: "buffer",
} as const;

function rethrowUncaught(err) {
Comment thread
cirospaciari marked this conversation as resolved.
Outdated
throw err;
}

function applyRejectUnauthorized(self, tls, rejectUnauthorized) {
if (typeof rejectUnauthorized !== "undefined") {
self._rejectUnauthorized = rejectUnauthorized !== false;
Expand Down Expand Up @@ -1183,10 +1210,31 @@ function onconnection(err, clientHandle) {
}
if (isTLS) initAcceptedTLSSocket(self, _socket);

// Mark server-accepted sockets as candidates for abandoned-socket teardown
// (SocketEmitEndNT schedules destroyAbandonedNT only when this is exactly
// false; client sockets never have it set so are excluded).
_socket[kReaderInterest] = false;
self.emit("connection", _socket);
if (!pauseOnConnect && !isTLS) {
_socket.read(0);
}
if (_socket.readableFlowing !== null || _socket.listenerCount("data") > 0 || _socket.listenerCount("readable") > 0) {
_socket[kReaderInterest] = true;
}
}

function destroyAbandonedNT(self) {
if (
self.destroyed ||
self[kReaderInterest] !== false ||
self.readableLength === 0 ||
self.readableFlowing !== null ||
self.listenerCount("data") > 0 ||
self.listenerCount("readable") > 0
) {
return;
}
self.destroySoon();
}

// TODO: SocketHandlers2 is a bad name but its temporary. reworking the Server in a followup PR
Expand Down Expand Up @@ -1813,7 +1861,10 @@ Socket.prototype._onTimeout = function () {

Object.defineProperty(Socket.prototype, "bufferSize", {
get: function () {
return this.writableLength;
// Node returns undefined once the handle is gone (after close).
if (this._handle) {
return this.writableLength;
}
},
});

Expand Down Expand Up @@ -2001,7 +2052,7 @@ Socket.prototype.connect = function connect(...args) {
const socket = connection._handle;
if (!upgradeDuplex && socket) {
// if is named pipe socket we can upgrade it using the same wrapper than we use for duplex
upgradeDuplex = isNamedPipeSocket(socket);
upgradeDuplex = isNamedPipeSocket(socket) || hasUnflushedWrites(connection);
}
if (upgradeDuplex) {
this[kupgraded] = connection;
Expand Down Expand Up @@ -2031,6 +2082,7 @@ Socket.prototype.connect = function connect(...args) {
const [raw, tls] = result;
// replace socket
connection._handle = raw;
raw[kAdoptedTLSRaw] = true;
this.once("end", this[kCloseRawConnection]);
raw.connecting = false;
this._handle = tls;
Expand All @@ -2051,7 +2103,7 @@ Socket.prototype.connect = function connect(...args) {
const socket = connection._handle;
if (!upgradeDuplex && socket) {
// if is named pipe socket we can upgrade it using the same wrapper than we use for duplex
upgradeDuplex = isNamedPipeSocket(socket);
upgradeDuplex = isNamedPipeSocket(socket) || hasUnflushedWrites(connection);
}
if (upgradeDuplex) {
this[kupgraded] = connection;
Expand All @@ -2077,6 +2129,7 @@ Socket.prototype.connect = function connect(...args) {
const [raw, tls] = result;
// replace socket
connection._handle = raw;
raw[kAdoptedTLSRaw] = true;
this.once("end", this[kCloseRawConnection]);
raw.connecting = false;
this._handle = tls;
Expand Down Expand Up @@ -2193,6 +2246,7 @@ Socket.prototype._destroy = function _destroy(err, callback) {
// this[kBytesRead] = this._handle.bytesRead;
this[kBytesWritten] = this._handle.bytesWritten;

const currentHandle = this._handle;
if (this.resetAndClosing) {
this.resetAndClosing = false;
// resetAndDestroy() must send an RST (not a graceful FIN) so the peer sees
Expand All @@ -2209,6 +2263,11 @@ Socket.prototype._destroy = function _destroy(err, callback) {
// Enqueue closing the socket as a microtask, so that the socket can be
// accessible when an `error` event is handled in the `next tick queue`.
queueMicrotask(() => closeSocketHandle(this, isException, true));
} else if (currentHandle[kAdoptedTLSRaw]) {
// Shared-fd TLS pair: defer the close two check-phase turns
// (test-tls-socket-close); see closeAdoptedTLSRawNT.
currentHandle.pause?.();
setImmediate(closeAdoptedTLSRawNT, currentHandle, this, isException);
} else {
closeSocketHandle(this, isException);
}
Expand Down Expand Up @@ -2279,6 +2338,13 @@ Object.defineProperty(Socket.prototype, "pending", {
},
});

// Queued/in-flight plain writes would be stranded on the retired TCP wrapper
// if the fd were adopted; such sockets use the stream-level TLS engine, whose
// ciphertext queues behind the pending writes (order + callbacks preserved).
function hasUnflushedWrites(connection) {
return connection.writableLength > 0 || connection[kwriteCallback] != null;
}

function drainOnreadTail(self, fromRead?) {
if (self[kOnreadTail] === undefined) return false;
if (fromRead) self[kOnreadReadRequested] = true;
Expand Down Expand Up @@ -2351,10 +2417,10 @@ Socket.prototype.pause = function pause() {
// state carried via `data` (mirrors tls.createServer's one-handler-for-all model).
Socket.prototype[Symbol.for("::bunUpgradeServerTLS::")] = function (connection, tls) {
const socket = connection._handle;
if (!socket) {
// A generic Duplex (or a not-yet-connected net.Socket) has no native fd
// to adopt into a TLS socket; run the TLS engine over the stream itself.
// The returned events feed the stream's bytes into the engine and back.
if (!socket || connection.encrypted || hasUnflushedWrites(connection)) {
// No adoptable fd (generic Duplex / not yet connected), TLS over TLS (the
// fd belongs to the outer SSL layer), or pending plain writes that must
// flush first: run the TLS engine over the stream itself.
const [result, events] = upgradeDuplexToTLS(connection, {
data: this,
tls,
Expand All @@ -2380,6 +2446,23 @@ Socket.prototype[Symbol.for("::bunUpgradeServerTLS::")] = function (connection,
this.destroy();
return;
}
// Writes may have been queued between the wrap and this tick (a user
// 'connection' listener runs after the server's): those bytes must flush
// before any TLS output, so fall back to the stream-level engine.
if (hasUnflushedWrites(connection)) {
const [result, events] = upgradeDuplexToTLS(connection, {
data: this,
tls,
socket: serverHandlersFor(this),
isServer: true,
});
connection.on("data", events[0]);
connection.on("end", events[1]);
connection.on("drain", events[2]);
connection.on("close", events[3]);
this._handle = result;
return;
}
// Bytes that already arrived before the wrap were pulled off the fd into
// the connection's readable buffer; hand them to the TLS engine so the
// handshake doesn't stall.
Expand All @@ -2398,6 +2481,7 @@ Socket.prototype[Symbol.for("::bunUpgradeServerTLS::")] = function (connection,
}
const [raw, tlsHandle] = result;
connection._handle = raw;
raw[kAdoptedTLSRaw] = true;
this.once("end", this[kCloseRawConnection]);
raw.connecting = false;
this._handle = tlsHandle;
Expand Down
39 changes: 37 additions & 2 deletions src/js/node/tls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,20 @@ function validateCiphers(ciphers: string, name: string = "options") {

const VALID_TLS_VERSIONS = new Set(["TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"]);

const SUPPORTED_ECDH_GROUPS = new Set([
"P-256",
"prime256v1",
"P-384",
"secp384r1",
"P-521",
"secp521r1",
"X25519",
"x25519",
"X25519Kyber768Draft00",
"X25519MLKEM768",
"MLKEM1024",
]);

// Subset of Node's configSecureContext() validations:
// https://github.com/nodejs/node/blob/843dc5f0d5ad/lib/internal/tls/secure-context.js#L318
function validateSecureContextOptions(options) {
Expand All @@ -204,7 +218,18 @@ function validateSecureContextOptions(options) {
validateString(sigalgs, "options.sigalgs");
if (sigalgs === "") throw $ERR_INVALID_ARG_VALUE("options.sigalgs", sigalgs);
}
if (ecdhCurve !== undefined) validateString(ecdhCurve, "options.ecdhCurve");
if (ecdhCurve !== undefined) {
validateString(ecdhCurve, "options.ecdhCurve");
if (ecdhCurve !== "auto") {
for (const curve of StringPrototypeSplit.$call(ecdhCurve, ":")) {
if (!SUPPORTED_ECDH_GROUPS.has(curve)) {
const err = new Error("Failed to set ECDH curve") as Error & { code: string };
err.code = "ERR_CRYPTO_OPERATION_FAILED";
throw err;
Comment thread
cirospaciari marked this conversation as resolved.
}
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// clientCertEngine must be a string (engine name); a provided engine then
// fails because BoringSSL (which Bun always uses) has no OpenSSL ENGINE
// support, matching Node's setClientCertEngine. Node:
Expand Down Expand Up @@ -957,6 +982,11 @@ TLSSocket.prototype.getTLSTicket = function getTLSTicket() {
return this._handle?.getTLSTicket?.();
};

// Lets net.ts's SNI dispatch recognize a raw native SecureContext returned by
// a socket-level SNICallback (`new tls.TLSSocket(sock, { isServer: true,
// SNICallback })`), where the handler's `this` is the socket, not a Server.
TLSSocket.prototype[kNativeSecureContextCtor] = NativeSecureContext;

TLSSocket.prototype.setKeyCert = function setKeyCert(context) {
// Serve this connection's identity from the given context (Node calls this
// from ALPNCallback/SNICallback before the certificate is sent). Accepts a
Expand Down Expand Up @@ -1434,7 +1464,12 @@ function Server(options, secureConnectionListener): void {
this._handshakeTimeout = handshakeTimeout;

this.on("connection", socket => {
if (!socket || socket.encrypted || socket instanceof TLSSocket) return;
// Skip only sockets this server's own native accept path already wrapped
// (those arrive as an encrypted TLSSocket with .server preassigned).
// Anything else - plain or TLS from another server - gets a server-side
// TLS layer, like Node's tls.Server wraps any injected duplex
// (node v26.3.0 lib/_tls_wrap.js, Server's connection listener).
if (!socket || (socket.encrypted && socket.server === this)) return;
let secureContext = this._sharedCreds;
if (!secureContext) {
try {
Expand Down
17 changes: 11 additions & 6 deletions src/runtime/api/SecureContext.classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@ export default [
// the regular key/cert/ca option plumbing can consume it.
parsePkcs12: { fn: "parse_pkcs12", length: 2 },
},
// No prototype surface — node:tls hands out the SecureContext object
// itself as `.context`. We deliberately do NOT expose the underlying
// SSL_CTX* to JS: a Number would lose precision above 2^53, and Node's
// `context._external` is a V8 External (opaque) used only by N-API
// addons that link OpenSSL directly, which Bun's BoringSSL build can't
// satisfy anyway.
// node:tls hands out the SecureContext object itself as `.context`. We
// deliberately do NOT expose the underlying SSL_CTX* to JS: a Number
// would lose precision above 2^53, and Node's `context._external` is a
// V8 External (opaque) used only by N-API addons that link OpenSSL
// directly, which Bun's BoringSSL build can't satisfy anyway.
proto: {
// Present so the accessor exists (and rejects a foreign receiver the
// way every other class getter does) rather than reading as a plain
// missing property; the value stays hidden, see above.
_external: {
getter: "get_external",
},
// `secureContext.context.addCACert(pem)` — Node's SecureContext exposes
// this so extra CAs can be appended to an existing context's store.
addCACert: {
Expand Down
Loading
Loading