Skip to content
Closed
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
45 changes: 24 additions & 21 deletions packages/bun-usockets/src/crypto/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -520,33 +520,36 @@ void us_ssl_enable_pending_events(SSL *ssl) {
SSL_set_ex_data(ssl, us_ssl_is_socket_ex_idx, (void *)1);
}

static int us_ssl_pop_pending(SSL *ssl, int idx, unsigned char *out, int out_cap) {
if (idx < 0) return 0;
static struct us_ssl_pending_session_t *
us_ssl_pop_pending(SSL *ssl, int idx, const unsigned char **out_data, size_t *out_len) {
if (idx < 0) return NULL;
struct us_ssl_pending_session_t *pending = SSL_get_ex_data(ssl, idx);
if (!pending) return 0;
if (!pending) return NULL;
SSL_set_ex_data(ssl, idx, pending->next);
int len = (int)pending->length;
if (len > out_cap) {
/* The parking sites cap entries (64 KB sessions, 4 KB+1 keylog lines) and
* callers pass buffers at least that large, so this is unreachable; drop
* the entry rather than overflow. */
len = 0;
} else {
memcpy(out, pending->data, (size_t)len);
}
us_free(pending);
return len;
pending->next = NULL;
*out_data = pending->data;
*out_len = pending->length;
return pending;
}

/* Detach the oldest parked session/keylog entry from `ssl` (entries come out
* in parking order) and point `*out_data`/`*out_len` at its payload. Returns
* NULL when the queue is empty. The entry is no longer reachable from the
* SSL, so the callback the caller hands it to may close the connection and
* SSL_free (which frees whatever is still queued) without invalidating it;
* the caller releases it with us_ssl_free_pending() afterwards. */
struct us_ssl_pending_session_t *
us_ssl_pop_pending_session(SSL *ssl, const unsigned char **out_data, size_t *out_len) {
return us_ssl_pop_pending(ssl, us_ssl_pending_session_idx, out_data, out_len);
}

/* Pop the oldest parked session/keylog entry into `out` (cap `out_cap`).
* Returns the byte length, or 0 when the queue is empty. Entries arrive in
* parking order; each pop hands over exactly one entry. */
int us_ssl_pop_pending_session(SSL *ssl, unsigned char *out, int out_cap) {
return us_ssl_pop_pending(ssl, us_ssl_pending_session_idx, out, out_cap);
struct us_ssl_pending_session_t *
us_ssl_pop_pending_keylog(SSL *ssl, const unsigned char **out_data, size_t *out_len) {
return us_ssl_pop_pending(ssl, us_ssl_pending_keylog_idx, out_data, out_len);
}

int us_ssl_pop_pending_keylog(SSL *ssl, unsigned char *out, int out_cap) {
return us_ssl_pop_pending(ssl, us_ssl_pending_keylog_idx, out, out_cap);
void us_ssl_free_pending(struct us_ssl_pending_session_t *entry) {
us_free(entry);
}

/* The resumable session most recently delivered via the new-session callback,
Expand Down
15 changes: 10 additions & 5 deletions packages/bun-usockets/src/libusockets.h
Original file line number Diff line number Diff line change
Expand Up @@ -554,12 +554,17 @@ long us_ssl_ctx_live_count(void);
int us_ssl_ctx_add_ca_cert(struct ssl_ctx_st *ctx, const char *content);
/* TLS-over-duplex / named-pipe SSL owners (no us_socket_t): opt an SSL into
* the parked new-session/keylog queues, then drain them with the pop calls
* after each SSL_read/SSL_do_handshake stack unwinds. Pop returns the entry
* length (0 = queue empty); entries are capped at 64 KB (sessions) and
* 4 KB+1 (keylog lines). */
* after each SSL_read/SSL_do_handshake stack unwinds. A pop detaches the
* oldest entry (NULL = queue empty) and exposes its payload; the entry stays
* valid across whatever the caller dispatches it to, even an SSL_free, until
* us_ssl_free_pending(). */
struct us_ssl_pending_session_t;
void us_ssl_enable_pending_events(struct ssl_st *ssl);
int us_ssl_pop_pending_session(struct ssl_st *ssl, unsigned char *out, int out_cap);
int us_ssl_pop_pending_keylog(struct ssl_st *ssl, unsigned char *out, int out_cap);
struct us_ssl_pending_session_t *us_ssl_pop_pending_session(
struct ssl_st *ssl, const unsigned char **out_data, size_t *out_len);
struct us_ssl_pending_session_t *us_ssl_pop_pending_keylog(
struct ssl_st *ssl, const unsigned char **out_data, size_t *out_len);
void us_ssl_free_pending(struct us_ssl_pending_session_t *entry);
/* 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);
Expand Down
177 changes: 87 additions & 90 deletions src/uws/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1039,20 +1039,9 @@ pub mod ssl_wrapper {
self.flags.set_fatal_error(true);
}

// flush the reading
if read > 0 {
log!("triggering data callback (read {})", read);
self.trigger_data_callback(&buffer[0..read]);
// The data callback may have closed the connection
if self.ssl.get().is_none() || self.flags.closed_notified() {
return false;
}
}
// A NewSessionTicket/keylog line that rode in ahead of the
// peer's close_notify is still parked; deliver it before the
// close tears the wrapper down (mirrors the C ZERO_RETURN path).
self.flush_pending_events(buffer);
if self.ssl.get().is_none() || self.flags.closed_notified() {
// Also with read == 0: a ticket that arrived with the
// close_notify is still parked.
Comment thread
robobun marked this conversation as resolved.
if !self.dispatch_read(&buffer[0..read]) {
return false;
}
self.trigger_close_callback();
Expand All @@ -1068,25 +1057,34 @@ pub mod ssl_wrapper {

read += usize::try_from(just_read).expect("int cast");
if read == buffer.len() {
log!(
"triggering data callback (read {}) and resetting read buffer",
read
);
// we filled the buffer
self.trigger_data_callback(&buffer[0..read]);
// The callback may have closed the connection - check before continuing
// Check ssl first as a proxy for whether we were deinited
if self.ssl.get().is_none() || self.flags.closed_notified() {
if !self.dispatch_read(&buffer[0..read]) {
return false;
}
log!("resetting read buffer");
read = 0;
}
}
// we finished reading
if read > 0 {
log!("triggering data callback (read {})", read);
self.trigger_data_callback(&buffer[0..read]);
// The callback may have closed the connection
if read > 0 && !self.dispatch_read(&buffer[0..read]) {
return false;
}
true
}

/// Deliver a batch of decrypted bytes, preceded by the sessions/keylog
/// lines parked while decrypting it (wire order, and what openssl.c's
/// `ssl_on_data` does): a data callback that closes the socket frees
/// the queues along with the SSL. Returns false once a callback closed
/// the wrapper.
Comment thread
robobun marked this conversation as resolved.
fn dispatch_read(&self, data: &[u8]) -> bool {
self.flush_pending_events();
if self.ssl.get().is_none() || self.flags.closed_notified() {
return false;
}
if !data.is_empty() {
log!("triggering data callback (read {})", data.len());
self.trigger_data_callback(data);
// Check ssl first as a proxy for whether we were deinited
if self.ssl.get().is_none() || self.flags.closed_notified() {
return false;
Expand Down Expand Up @@ -1143,64 +1141,48 @@ pub mod ssl_wrapper {
self.handle_writing(&mut buffer);
}

// The SSL_do_handshake/SSL_read calls above may have parked
// new-session tickets / keylog lines (BoringSSL surfaces them
// mid-read, where dispatching JS could free the SSL out from
// under the caller). The stack has unwound here, so hand them
// to the owner - same ordering as the C path's
// ssl_flush_pending_session: handshake/data callbacks first,
// then sessions.
self.flush_pending_events(&mut buffer);
// What the handshake, or a read that produced no application
// data, parked; `dispatch_read` covers the rest.
Comment thread
robobun marked this conversation as resolved.
self.flush_pending_events();
}
}

/// Drain the parked new-session / keylog queues into the owner's
/// callbacks. Only SSLs whose handlers opted in ever park (see
/// `init_with_ctx`), so this is a no-op FFI probe otherwise. The
/// callbacks run JS which may close the wrapper; `self.ssl` is
/// re-checked between pops.
fn flush_pending_events(&self, buffer: &mut [u8; BUFFER_SIZE]) {
if self.handlers.get().on_session.is_some() {
loop {
let Some(ssl) = self.ssl.get() else { return };
// SAFETY: ssl is live (checked above); buffer is writable
// for BUFFER_SIZE bytes, which covers the 64 KB parking cap.
let len = unsafe {
us_ssl_pop_pending_session(
ssl.as_ptr(),
buffer.as_mut_ptr(),
c_int::try_from(BUFFER_SIZE).expect("int cast"),
)
};
if len <= 0 {
break;
}
let handlers = self.handlers.get();
if let Some(on_session) = handlers.on_session {
on_session(handlers.ctx, &buffer[..len as usize]);
}
}
/// Deliver the new-session / keylog entries openssl.c parked on the
/// SSL during `SSL_read`/`SSL_do_handshake` (only for handlers that
/// opted in, see `init_with_ctx`); sessions first, like the C path.
Comment thread
robobun marked this conversation as resolved.
fn flush_pending_events(&self) {
let handlers = self.handlers.get();
if let Some(on_session) = handlers.on_session {
self.drain_pending(us_ssl_pop_pending_session, handlers.ctx, on_session);
}
if self.handlers.get().on_keylog.is_some() {
loop {
let Some(ssl) = self.ssl.get() else { return };
// SAFETY: same as the session pop above; keylog entries
// are capped at 4 KB+1, well within BUFFER_SIZE.
let len = unsafe {
us_ssl_pop_pending_keylog(
ssl.as_ptr(),
buffer.as_mut_ptr(),
c_int::try_from(BUFFER_SIZE).expect("int cast"),
)
};
if len <= 0 {
break;
}
let handlers = self.handlers.get();
if let Some(on_keylog) = handlers.on_keylog {
on_keylog(handlers.ctx, &buffer[..len as usize]);
}
}
if let Some(on_keylog) = handlers.on_keylog {
self.drain_pending(us_ssl_pop_pending_keylog, handlers.ctx, on_keylog);
}
}

/// Each entry is detached from the SSL before `deliver` runs, so the
/// callback may close the wrapper (`SSL_free` takes the rest of the
/// queue with it) without invalidating the payload in flight.
Comment thread
robobun marked this conversation as resolved.
fn drain_pending(&self, pop: PopPendingFn, ctx: T, deliver: fn(T, &[u8])) {
while !self.flags.closed_notified() {
let Some(ssl) = self.ssl.get() else { return };
let mut data: *const u8 = core::ptr::null();
let mut len: usize = 0;
// SAFETY: `ssl` is live (checked above); the out-params are locals.
let Some(entry) =
NonNull::new(unsafe { pop(ssl.as_ptr(), &raw mut data, &raw mut len) })
else {
return;
};
let entry = scopeguard::guard(entry, |entry| {
// SAFETY: `pop` transferred the entry to us; freed exactly once here.
unsafe { us_ssl_free_pending(entry.as_ptr()) }
});
// SAFETY: `data`/`len` describe the payload inside `*entry`,
// which stays allocated until the guard drops below.
let payload = unsafe { bun_core::ffi::slice(data, len) };
deliver(ctx, payload);
drop(entry);
}
}
}
Expand All @@ -1220,6 +1202,19 @@ pub mod ssl_wrapper {
1
}

bun_opaque::opaque_ffi! {
/// A parked new-session / keylog entry (openssl.c); owned by whoever
/// popped it until `us_ssl_free_pending`.
Comment thread
robobun marked this conversation as resolved.
struct us_ssl_pending_session_t;
}

/// Shape of `us_ssl_pop_pending_session` / `us_ssl_pop_pending_keylog`.
type PopPendingFn = unsafe extern "C" fn(
ssl: *mut boring_sys::SSL,
out_data: *mut *const u8,
out_len: *mut usize,
) -> *mut us_ssl_pending_session_t;

unsafe extern "C" {
/// Process-wide bundled root store from `root_certs.cpp` — built once and
/// up_ref'd per consumer so the ~150-cert load happens once total, not per
Expand All @@ -1234,20 +1229,22 @@ pub mod ssl_wrapper {
/// SSLs without the marker).
// SAFETY (unsafe fn): `ssl` must be a live `SSL*`.
fn us_ssl_enable_pending_events(ssl: *mut boring_sys::SSL);
/// Pop the oldest parked session/keylog entry into `out`; returns the
/// entry length or 0 when the queue is empty.
// SAFETY (unsafe fn): `ssl` live; `out` writable for `out_cap` bytes.
/// Detach the oldest parked entry and expose its payload; null when
/// the queue is empty.
// SAFETY (unsafe fn): `ssl` live; `out_data`/`out_len` writable.
Comment thread
robobun marked this conversation as resolved.
fn us_ssl_pop_pending_session(
ssl: *mut boring_sys::SSL,
out: *mut u8,
out_cap: c_int,
) -> c_int;
// SAFETY (unsafe fn): `ssl` live; `out` writable for `out_cap` bytes.
out_data: *mut *const u8,
out_len: *mut usize,
) -> *mut us_ssl_pending_session_t;
// SAFETY (unsafe fn): same as `us_ssl_pop_pending_session`.
fn us_ssl_pop_pending_keylog(
ssl: *mut boring_sys::SSL,
out: *mut u8,
out_cap: c_int,
) -> c_int;
out_data: *mut *const u8,
out_len: *mut usize,
) -> *mut us_ssl_pending_session_t;
// SAFETY (unsafe fn): `entry` came from a pop above and is freed once.
fn us_ssl_free_pending(entry: *mut us_ssl_pending_session_t);
}
}

Expand Down
Loading
Loading