Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
65 changes: 63 additions & 2 deletions packages/bun-usockets/src/crypto/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,12 @@ static int us_ssl_pending_keylog_idx = -1;
* received NewSessionTicket, so SSL_get_session() alone gives an unresumable
* snapshot; node:tls's getSession()/getTLSTicket() read from here instead. */
static int us_ssl_new_session_ref_idx = -1;
/* Optional per-SSL sink for resumable sessions: an owner pointer plus a
* callback that receives each SSL_SESSION_up_ref'd session. Checked before the
* us_ssl_is_socket_ex_idx opt-in so consumers that don't surface a JS
* 'session' event (fetch) can still cache without paying the serialized
* pending-session queue. */
static int us_ssl_session_sink_idx = -1;
#ifdef _WIN32
static INIT_ONCE us_ex_idx_once = INIT_ONCE_STATIC_INIT;
#else
Expand Down Expand Up @@ -282,6 +288,20 @@ static void us_ssl_new_session_ref_free(void *parent, void *ptr, CRYPTO_EX_DATA
(void)parent; (void)ad; (void)index; (void)argl; (void)argp;
if (ptr) SSL_SESSION_free((SSL_SESSION *)ptr);
}

struct us_ssl_session_sink_t {
void *owner;
void (*on_new_session)(void *owner, SSL_SESSION *session);
void (*on_free)(void *owner);
};
static void us_ssl_session_sink_free(void *parent, void *ptr, CRYPTO_EX_DATA *ad,
int index, long argl, void *argp) {
(void)parent; (void)ad; (void)index; (void)argl; (void)argp;
if (!ptr) return;
struct us_ssl_session_sink_t *sink = ptr;
if (sink->on_free) sink->on_free(sink->owner);
us_free(sink);
}
/* NSS key-log lines are produced from inside SSL_do_handshake/SSL_read, so
* they are parked on the SSL the same way new sessions are and delivered once
* the read unwinds. The stored bytes already carry the trailing newline Node
Expand Down Expand Up @@ -334,12 +354,21 @@ static void ssl_flush_pending_keylog(struct us_socket_t *s) {
}

static int us_ssl_new_session_cb(SSL *ssl, SSL_SESSION *session) {
/* The session sink is the cheap path: hand the session to an owner-provided
* callback (one up_ref, no i2d serialize, no queue). Used by the HTTP
* client's per-origin session cache. */
struct us_ssl_session_sink_t *sink = SSL_get_ex_data(ssl, us_ssl_session_sink_idx);
if (sink && sink->on_new_session) {
SSL_SESSION_up_ref(session);
sink->on_new_session(sink->owner, session);
return 0;
}
/* Park only for consumers that will drain the queue: SSLs attached to a
* real us_socket_t (flushed into us_dispatch_session once the read unwinds)
* and SSLs whose owner opted in via us_ssl_enable_pending_events (the
* Rust SSLWrapper behind TLS-over-duplex / named pipes, which polls
* us_ssl_pop_pending_session after its reads). Everything else (fetch,
* WebSocket tunnels) has no consumer - don't queue. */
* us_ssl_pop_pending_session after its reads). Everything else (WebSocket
* tunnels) has no consumer - don't queue. */
if (!SSL_get_ex_data(ssl, us_ssl_is_socket_ex_idx)) {
return 0;
}
Expand Down Expand Up @@ -421,6 +450,7 @@ static void us_ex_idx_init(void) {
us_ssl_pending_session_idx = SSL_get_ex_new_index(0, NULL, NULL, NULL, us_ssl_pending_session_free);
us_ssl_pending_keylog_idx = SSL_get_ex_new_index(0, NULL, NULL, NULL, us_ssl_pending_session_free);
us_ssl_new_session_ref_idx = SSL_get_ex_new_index(0, NULL, NULL, NULL, us_ssl_new_session_ref_free);
us_ssl_session_sink_idx = SSL_get_ex_new_index(0, NULL, NULL, NULL, us_ssl_session_sink_free);
}

#ifdef _WIN32
Expand All @@ -444,6 +474,37 @@ static inline int us_ssl_ctx_ex_idx(void) {
return us_ctx_ex_idx;
}

/* Install a session sink on `ssl`: each resumable session reaching the
* new-session callback is SSL_SESSION_up_ref'd and passed to `on_new_session`
* (which takes ownership of that reference). `on_free(owner)` runs once on
* SSL_free. Replacing an existing sink first frees the old one. */
void us_ssl_set_session_sink(SSL *ssl, void *owner,
void (*on_new_session)(void *, SSL_SESSION *),
void (*on_free)(void *)) {
us_ex_idx_ensure();
struct us_ssl_session_sink_t *sink = us_malloc(sizeof(*sink));
if (!sink) {
if (on_free) on_free(owner);
return;
}
sink->owner = owner;
sink->on_new_session = on_new_session;
sink->on_free = on_free;
struct us_ssl_session_sink_t *old = SSL_get_ex_data(ssl, us_ssl_session_sink_idx);
SSL_set_ex_data(ssl, us_ssl_session_sink_idx, sink);
if (old) {
if (old->on_free) old->on_free(old->owner);
us_free(old);
}
}

/* The `owner` pointer installed via us_ssl_set_session_sink, or NULL. */
void *us_ssl_get_session_sink_owner(SSL *ssl) {
if (us_ssl_session_sink_idx < 0) return NULL;
struct us_ssl_session_sink_t *sink = SSL_get_ex_data(ssl, us_ssl_session_sink_idx);
return sink ? sink->owner : NULL;
}

/* TLS-over-duplex / named-pipe owners (the Rust SSLWrapper): opt this SSL
* into the parked session/keylog queues so us_ssl_new_session_cb /
* us_ssl_keylog_cb collect them. There is no us_socket_t to flush into
Expand Down
7 changes: 7 additions & 0 deletions packages/bun-usockets/src/libusockets.h
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,13 @@ int us_ssl_pop_pending_keylog(struct ssl_st *ssl, unsigned char *out, int out_ca
/* The resumable session most recently delivered via the new-session callback,
* or NULL if none. Borrowed; valid until the next NewSessionTicket or SSL_free. */
struct ssl_session_st *us_ssl_get_new_session(struct ssl_st *ssl);
/* Per-SSL session sink: each resumable session reaching the new-session
* callback is SSL_SESSION_up_ref'd and handed to on_new_session (which takes
* ownership of that reference). on_free(owner) runs once on SSL_free. */
void us_ssl_set_session_sink(struct ssl_st *ssl, void *owner,
void (*on_new_session)(void *, struct ssl_session_st *),
void (*on_free)(void *));
void *us_ssl_get_session_sink_owner(struct ssl_st *ssl);

/* Public interfaces for loops */

Expand Down
24 changes: 16 additions & 8 deletions src/boringssl/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,30 +94,38 @@ pub unsafe fn ssl_ctx_setup(ctx: *mut boring::SSL_CTX) {
// into the process, including pthreads locks. Failing to meet these constraints
// may result in deadlocks, crashes, or memory corruption.

// Routed through `default_alloc` (mimalloc, or libc under `cfg(bun_asan)`)
// rather than `mimalloc` directly. The BoringSSL build already drops
// `BORINGSSL_REQUIRE_MEMORY_HOOKS` under ASAN so Mach-O/COFF fall back to
// libc, but on ELF the weak hook symbols still resolve to these definitions;
// hard-coding mimalloc here put every `OPENSSL_malloc` allocation outside
// LeakSanitizer's scanned set and any libc-backed allocation reachable only
// via one (an SSL's ex_data array, the per-SSL session-sink owner it holds)
// was reported as a leak at exit.
Comment thread
robobun marked this conversation as resolved.
#[unsafe(no_mangle)]
pub(crate) extern "C" fn OPENSSL_memory_alloc(size: usize) -> *mut c_void {
bun_alloc::mimalloc::mi_malloc(size)
bun_alloc::default_alloc::malloc(size)
}

// BoringSSL always expects memory to be zero'd
/// # Safety
/// `ptr` must be non-null and have been returned by `OPENSSL_memory_alloc`
/// (i.e. `mi_malloc`); BoringSSL guarantees both for this hook.
/// `ptr` must be non-null and returned by `OPENSSL_memory_alloc`; BoringSSL
/// guarantees both for this hook.
Comment thread
robobun marked this conversation as resolved.
#[unsafe(no_mangle)]
pub(crate) unsafe extern "C" fn OPENSSL_memory_free(ptr: *mut c_void) {
// SAFETY: BoringSSL guarantees ptr is non-null and was returned by
// OPENSSL_memory_alloc above (i.e. mi_malloc).
// OPENSSL_memory_alloc above.
unsafe {
let len = bun_alloc::usable_size(ptr.cast());
let len = bun_alloc::default_alloc::usable_size(ptr);
ptr::write_bytes(ptr.cast::<u8>(), 0, len);
bun_alloc::mimalloc::mi_free(ptr);
bun_alloc::default_alloc::free(ptr);
}
}

#[unsafe(no_mangle)]
pub(crate) extern "C" fn OPENSSL_memory_get_size(ptr: *const c_void) -> usize {
// ptr was returned by mi_malloc (or is null, which usable_size handles).
bun_alloc::usable_size(ptr.cast())
// SAFETY: ptr was returned by OPENSSL_memory_alloc (null-safe).
unsafe { bun_alloc::default_alloc::usable_size(ptr) }
}

pub use bun_sys::posix::INET6_ADDRSTRLEN;
Expand Down
3 changes: 1 addition & 2 deletions src/bun_alloc/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,7 @@ pub mod default_alloc {
/// # Safety
/// `ptr` must be null or a live allocation from the default allocator.
#[inline]
#[cfg(any(debug_assertions, bun_asan))]
pub(crate) unsafe fn usable_size(ptr: *const c_void) -> usize {
pub unsafe fn usable_size(ptr: *const c_void) -> usize {
if ptr.is_null() {
return 0;
}
Expand Down
1 change: 1 addition & 0 deletions src/bun_core/env_var.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ pub mod feature_flag {
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_ASYNC_TRANSPILER, "BUN_FEATURE_FLAG_DISABLE_ASYNC_TRANSPILER", {});
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_ISOLATION_SOURCE_CACHE, "BUN_FEATURE_FLAG_DISABLE_ISOLATION_SOURCE_CACHE", {});
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_DNS_CACHE, "BUN_FEATURE_FLAG_DISABLE_DNS_CACHE", {});
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_FETCH_TLS_SESSION_CACHE, "BUN_FEATURE_FLAG_DISABLE_FETCH_TLS_SESSION_CACHE", {});
new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_DNS_CACHE_LIBINFO, "BUN_FEATURE_FLAG_DISABLE_DNS_CACHE_LIBINFO", {});
// Force the event loop to use epoll_pwait(2) instead of epoll_pwait2(2).
// Escape hatch for seccomp policies that block syscall 441 without
Expand Down
10 changes: 9 additions & 1 deletion src/http/HTTPContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use bun_uws as uws;
bun_core::declare_scope!(HTTPContext, hidden);

const POOL_SIZE: usize = 64;
const MAX_KEEPALIVE_HOSTNAME: usize = 128;
pub(crate) const MAX_KEEPALIVE_HOSTNAME: usize = 128;

/// The const-generic `SSL` is load-bearing for monomorphization (gates hot
/// inner-loop branches); do not demote to a runtime bool.
Expand Down Expand Up @@ -55,6 +55,8 @@ pub struct HTTPContext<const SSL: bool> {
// into the box interior; unboxing would dangle it on `Vec` realloc.
#[expect(clippy::vec_box)]
pub(crate) pending_h2_connects: Vec<Box<h2::PendingConnect>>,
/// Client-side TLS session cache; populated only when `SSL`.
pub(crate) session_cache: crate::session_cache::SessionCache,
}

// Intrusive refcount:
Expand Down Expand Up @@ -1177,6 +1179,12 @@ impl<const SSL: bool> Handler<SSL> {
// `client` again.
return;
}
// Peer chain + hostname verified: let the session sink
// flush its pending TLS 1.2 ticket (parked before this
// dispatch) and cache later TLS 1.3 tickets directly.
// SAFETY: `ssl` is the live handle for this socket on the
// HTTP thread.
unsafe { crate::session_cache::arm(ssl) };
}

return client.first_call::<SSL>(socket);
Expand Down
3 changes: 3 additions & 0 deletions src/http/HTTPThread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ impl HttpThread {
secure: None,
active_h2_sessions: Vec::new(),
pending_h2_connects: Vec::new(),
session_cache: crate::session_cache::SessionCache::new(),
},
https_context: NewHttpContext::<true> {
ref_count: Cell::new(1),
Expand All @@ -170,6 +171,7 @@ impl HttpThread {
secure: None,
active_h2_sessions: Vec::new(),
pending_h2_connects: Vec::new(),
session_cache: crate::session_cache::SessionCache::new(),
},
lazy_https_init: None,
queued_tasks: Queue::new(),
Expand Down Expand Up @@ -530,6 +532,7 @@ impl HttpThread {
secure: None,
active_h2_sessions: Vec::new(),
pending_h2_connects: Vec::new(),
session_cache: crate::session_cache::SessionCache::new(),
}));
if let Err(err) = custom_context.init_with_client_config(client) {
// `init_with_client_config` fails before `group.init()` runs.
Expand Down
24 changes: 24 additions & 0 deletions src/http/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ pub mod lshpack;
pub mod proxy_tunnel;
#[path = "SendFile.rs"]
pub mod send_file;
#[path = "session_cache.rs"]
pub mod session_cache;
#[path = "Signals.rs"]
pub mod signals;
#[path = "ThreadSafeStreamBuffer.rs"]
Expand Down Expand Up @@ -1851,6 +1853,28 @@ impl<'a> HTTPClient<'a> {
host_z,
self.alpn_offer(),
);

if crate::session_cache::eligible(self) {
let want_tunnel = self.http_proxy.is_some() && self.url.is_https();
// SAFETY: `ssl_ptr` is live and pre-handshake (guarded by
// `SSL_is_init_finished == 0` above); `get_ssl_ctx` returns
// the static `https_context` or the heap context this
// client holds a strong ref on, both of which outlive
// every SSL attached to their socket group.
unsafe {
crate::session_cache::install(
ssl_ptr,
self.get_ssl_ctx::<true>(),
self.connected_url.hostname,
self.connected_url.get_port_auto(),
if want_tunnel || self.http_proxy.is_none() {
self.proxy_auth_hash()
} else {
0
},
);
}
}
}
} else {
self.first_call::<IS_SSL>(socket);
Expand Down
Loading
Loading