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
3 changes: 3 additions & 0 deletions packages/bun-usockets/src/crypto/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,7 @@ int us_ssl_ctx_use_privatekey_content(SSL_CTX *ctx, const char *content,
int reason_code, ret = 0;
BIO *in;
EVP_PKEY *pkey = NULL;
if (content == NULL) return 0;
in = BIO_new_mem_buf(content, strlen(content));
if (in == NULL) {
OPENSSL_PUT_ERROR(SSL, ERR_R_BUF_LIB);
Expand Down Expand Up @@ -930,6 +931,7 @@ int add_ca_cert_to_ctx_store(SSL_CTX *ctx, const char *content,
X509 *x = NULL;
ERR_clear_error(); // clear error stack for SSL_CTX_use_certificate()
int count = 0;
if (content == NULL) return 0;
BIO *in = BIO_new_mem_buf(content, strlen(content));
if (in == NULL) {
OPENSSL_PUT_ERROR(SSL, ERR_R_BUF_LIB);
Expand Down Expand Up @@ -963,6 +965,7 @@ int us_ssl_ctx_use_certificate_chain(SSL_CTX *ctx, const char *content) {

ERR_clear_error(); // clear error stack for SSL_CTX_use_certificate()

if (content == NULL) return 0;
Comment thread
claude[bot] marked this conversation as resolved.
in = BIO_new_mem_buf(content, strlen(content));
if (in == NULL) {
OPENSSL_PUT_ERROR(SSL, ERR_R_BUF_LIB);
Expand Down
108 changes: 72 additions & 36 deletions src/bun.js/api/server/SSLConfig.zig
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,23 @@ client_renegotiation_window: u32 = 0,
requires_custom_request_ctx: bool = false,
is_using_default_ciphers: bool = true,
low_memory_mode: bool = false,
ref_count: RC = .init(),
cached_hash: u64 = 0,

const RC = bun.ptr.ThreadSafeRefCount(@This(), "ref_count", destroy, .{});
pub const ref = RC.ref;
pub const deref = RC.deref;
/// Atomic shared pointer with weak support. Refcounting and allocation are
/// managed non-intrusively by `bun.ptr.shared`; the SSLConfig struct itself
/// has no refcount field.
pub const SharedPtr = bun.ptr.shared.WithOptions(*SSLConfig, .{
.atomic = true,
.allow_weak = true,
});

const WeakPtr = SharedPtr.Weak;

/// Extract the raw `*SSLConfig` from an optional SharedPtr for pointer-equality
/// comparison (interned configs have stable addresses).
pub inline fn rawPtr(maybe_shared: ?SharedPtr) ?*SSLConfig {
return if (maybe_shared) |s| s.get() else null;
}

const ReadFromBlobError = bun.JSError || error{
NullStore,
Expand Down Expand Up @@ -119,7 +130,7 @@ pub fn forClientVerification(this: SSLConfig) SSLConfig {

pub fn isSame(this: *const SSLConfig, other: *const SSLConfig) bool {
inline for (comptime std.meta.fields(SSLConfig)) |field| {
if (comptime std.mem.eql(u8, field.name, "ref_count") or std.mem.eql(u8, field.name, "cached_hash")) continue;
if (comptime std.mem.eql(u8, field.name, "cached_hash")) continue;
const first = @field(this, field.name);
const second = @field(other, field.name);
switch (field.type) {
Expand Down Expand Up @@ -171,7 +182,15 @@ fn freeString(string: *?[*:0]const u8) void {
string.* = null;
}

/// Destructor. Called by `bun.ptr.shared` on strong 1->0 for interned configs,
/// and directly on value-type configs (e.g. `ServerConfig.ssl_config`).
///
/// For interned configs, we MUST remove from the registry before freeing the
/// string fields, since concurrent `intern()` calls may read those fields for
/// content comparison while we're still in the map. For non-interned configs,
/// `remove()` is a cheap no-op (pointer-identity check fails).
pub fn deinit(this: *SSLConfig) void {
GlobalRegistry.remove(this);
bun.meta.useAllFields(SSLConfig, .{
.server_name = freeString(&this.server_name),
.key_file_name = freeString(&this.key_file_name),
Expand All @@ -192,7 +211,6 @@ pub fn deinit(this: *SSLConfig) void {
.requires_custom_request_ctx = {},
.is_using_default_ciphers = {},
.low_memory_mode = {},
.ref_count = {},
.cached_hash = {},
});
}
Expand Down Expand Up @@ -231,7 +249,6 @@ pub fn clone(this: *const SSLConfig) SSLConfig {
.requires_custom_request_ctx = this.requires_custom_request_ctx,
.is_using_default_ciphers = this.is_using_default_ciphers,
.low_memory_mode = this.low_memory_mode,
.ref_count = .init(),
.cached_hash = 0,
};
}
Expand All @@ -240,7 +257,7 @@ pub fn contentHash(this: *SSLConfig) u64 {
if (this.cached_hash != 0) return this.cached_hash;
var hasher = std.hash.Wyhash.init(0);
inline for (comptime std.meta.fields(SSLConfig)) |field| {
if (comptime std.mem.eql(u8, field.name, "ref_count") or std.mem.eql(u8, field.name, "cached_hash")) continue;
if (comptime std.mem.eql(u8, field.name, "cached_hash")) continue;
const value = @field(this, field.name);
switch (field.type) {
?[*:0]const u8 => {
Expand Down Expand Up @@ -269,13 +286,11 @@ pub fn contentHash(this: *SSLConfig) u64 {
return this.cached_hash;
}

/// Called by the RC mixin when refcount reaches 0.
fn destroy(this: *SSLConfig) void {
GlobalRegistry.remove(this);
this.deinit();
bun.default_allocator.destroy(this);
}

/// Weak dedup cache. Each map entry stores a weak pointer on its key's
/// backing allocation. `upgrade()` on that weak pointer is memory-safe
/// because the weak ref keeps the allocation alive (even if strong==0 and
/// `deinit()` is running on another thread). The mutex only protects map
/// structure and the invariant that entry content is intact while in the map.
pub const GlobalRegistry = struct {
const MapContext = struct {
pub fn hash(_: @This(), key: *SSLConfig) u32 {
Expand All @@ -287,38 +302,59 @@ pub const GlobalRegistry = struct {
};

var mutex: bun.Mutex = .{};
var configs: std.ArrayHashMapUnmanaged(*SSLConfig, void, MapContext, true) = .empty;
var configs: std.ArrayHashMapUnmanaged(*SSLConfig, WeakPtr, MapContext, true) = .empty;

/// Takes a by-value SSLConfig, wraps it in a `SharedPtr` (strong=1), and
/// either returns an existing equivalent (upgraded) or the new one. Either
/// way, caller owns exactly one strong ref on the result.
///
/// The returned `SharedPtr` must eventually be `.deinit()`d.
pub fn intern(config: SSLConfig) SharedPtr {
var new_shared = SharedPtr.new(config);
const new_ptr = new_shared.get();

// Deferred cleanup MUST run after `mutex.unlock()` (deinit re-locks
// the registry mutex via `SSLConfig.deinit -> remove`).
var dispose_new: ?SharedPtr = null;
var dispose_old_weak: ?WeakPtr = null;
defer if (dispose_new) |*s| s.deinit();
defer if (dispose_old_weak) |*w| w.deinit();

/// Takes ownership of a heap-allocated SSLConfig.
/// If an identical config already exists in the registry, the new one is freed
/// and the existing one is returned (with refcount incremented).
/// If no match, the new config is registered and returned.
pub fn intern(new_config: *SSLConfig) *SSLConfig {
mutex.lock();
defer mutex.unlock();

// Look up by content hash/equality
const gop = bun.handleOom(configs.getOrPutContext(bun.default_allocator, new_config, .{}));
const gop = bun.handleOom(configs.getOrPutContext(bun.default_allocator, new_ptr, .{}));
if (gop.found_existing) {
// Identical config already exists - free the new one, return existing
const existing = gop.key_ptr.*;
new_config.ref_count.clearWithoutDestructor();
new_config.deinit();
bun.default_allocator.destroy(new_config);
existing.ref();
return existing;
if (gop.value_ptr.upgrade()) |existing_shared| {
// Existing config is still alive; dispose the new duplicate.
dispose_new = new_shared;
return existing_shared;
}
// strong==0: existing is dying. Its `deinit()` is blocked in
// `remove()` waiting for this mutex, so content is still intact
// (fields not yet freed). Replace the slot; the dying config's
// `remove()` will pointer-mismatch and no-op when it runs.
dispose_old_weak = gop.value_ptr.*;
gop.key_ptr.* = new_ptr;
}

// New config - it's already inserted by getOrPut
// refcount is already 1 from initialization
return new_config;
gop.value_ptr.* = new_shared.cloneWeak();
return new_shared;
}

/// Remove a config from the registry. Called when refcount reaches 0.
/// Called from `SSLConfig.deinit()` on strong 1->0. If `intern()` replaced
/// our slot while we blocked on the mutex, the pointer-identity check
/// fails and we skip (intern already disposed our weak ref).
///
/// No-op for configs that were never interned.
fn remove(config: *SSLConfig) void {
mutex.lock();
defer mutex.unlock();
_ = configs.swapRemoveContext(config, .{});
if (configs.count() == 0) return;
const idx = configs.getIndexContext(config, .{}) orelse return;
if (configs.keys()[idx] != config) return;
var weak = configs.values()[idx];
configs.swapRemoveAt(idx);
weak.deinit();
}
};

Expand Down
10 changes: 4 additions & 6 deletions src/bun.js/webcore/fetch.zig
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ fn fetchImpl(
};
var url_type = URLType.remote;

var ssl_config: ?*SSLConfig = null;
var ssl_config: ?SSLConfig.SharedPtr = null;
var reject_unauthorized = vm.getTLSRejectUnauthorized();
var check_server_identity: JSValue = .zero;

Expand Down Expand Up @@ -273,9 +273,9 @@ fn fetchImpl(
range = null;
}

if (ssl_config) |conf| {
if (ssl_config) |*conf| {
conf.deinit();
ssl_config = null;
conf.deref();
}
}

Expand Down Expand Up @@ -465,10 +465,8 @@ fn fetchImpl(
is_error = true;
return .zero;
}) |config| {
const ssl_config_object = bun.handleOom(bun.default_allocator.create(SSLConfig));
ssl_config_object.* = config;
// Intern via GlobalRegistry for deduplication and pointer equality
break :extract_ssl_config SSLConfig.GlobalRegistry.intern(ssl_config_object);
break :extract_ssl_config SSLConfig.GlobalRegistry.intern(config);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/bun.js/webcore/fetch/FetchTasklet.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1287,7 +1287,7 @@ pub const FetchTasklet = struct {
hostname: ?[]u8 = null,
check_server_identity: jsc.Strong.Optional = .empty,
unix_socket_path: ZigString.Slice,
ssl_config: ?*SSLConfig = null,
ssl_config: ?SSLConfig.SharedPtr = null,
upgraded_connection: bool = false,
};

Expand Down
12 changes: 5 additions & 7 deletions src/http.zig
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ progress_node: ?*Progress.Node = null,
flags: Flags = Flags{},

state: InternalState = .{},
tls_props: ?*SSLConfig = null,
tls_props: ?SSLConfig.SharedPtr = null,
/// The custom SSL context used for this request (null = default context).
/// Set by HTTPThread.connect() when using custom TLS configs.
custom_ssl_ctx: ?*NewHTTPContext(true) = null,
Expand Down Expand Up @@ -518,11 +518,9 @@ pub fn deinit(this: *HTTPClient) void {
this.proxy_tunnel = null;
tunnel.detachAndDeref();
}
// Release our reference on the interned SSLConfig
if (this.tls_props) |config| {
config.deref();
this.tls_props = null;
}
// Release our strong ref on the interned SSLConfig
if (this.tls_props) |*tls| tls.deinit();
this.tls_props = null;
this.unix_socket_path.deinit();
this.unix_socket_path = jsc.ZigString.Slice.empty;
}
Expand Down Expand Up @@ -1454,7 +1452,7 @@ pub fn closeAndFail(this: *HTTPClient, err: anyerror, comptime is_ssl: bool, soc
fn startProxyHandshake(this: *HTTPClient, comptime is_ssl: bool, socket: NewHTTPContext(is_ssl).HTTPSocket, start_payload: []const u8) void {
log("startProxyHandshake", .{});
// if we have options we pass them (ca, reject_unauthorized, etc) otherwise use the default
const ssl_options = if (this.tls_props != null) this.tls_props.?.* else jsc.API.ServerConfig.SSLConfig.zero;
const ssl_options = if (this.tls_props) |tls| tls.get().* else jsc.API.ServerConfig.SSLConfig.zero;
ProxyTunnel.start(this, is_ssl, socket, ssl_options, start_payload);
}

Expand Down
2 changes: 1 addition & 1 deletion src/http/AsyncHTTP.zig
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ pub const Options = struct {
disable_keepalive: ?bool = null,
disable_decompression: ?bool = null,
reject_unauthorized: ?bool = null,
tls_props: ?*SSLConfig = null,
tls_props: ?SSLConfig.SharedPtr = null,
};

const Preconnect = struct {
Expand Down
41 changes: 16 additions & 25 deletions src/http/HTTPContext.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ pub fn NewHTTPContext(comptime ssl: bool) type {
/// If you set `rejectUnauthorized` to `false`, the connection fails to verify,
did_have_handshaking_error_while_reject_unauthorized_is_false: bool = false,
/// The interned SSLConfig this socket was created with (null = default context).
/// Holds a ref while the socket is in the keepalive pool.
ssl_config: ?*SSLConfig = null,
/// Owns a strong ref while the socket is in the keepalive pool.
ssl_config: ?SSLConfig.SharedPtr = null,
/// The context that owns this pooled socket's memory (for returning to correct pool).
owner: *Context,
};
Expand Down Expand Up @@ -96,10 +96,8 @@ pub fn NewHTTPContext(comptime ssl: bool) type {
var iter = this.pending_sockets.used.iterator(.{ .kind = .set });
while (iter.next()) |idx| {
const pooled = this.pending_sockets.at(@intCast(idx));
if (pooled.ssl_config) |config| {
config.deref();
pooled.ssl_config = null;
}
if (pooled.ssl_config) |*s| s.deinit();
pooled.ssl_config = null;
pooled.http_socket.close(.failure);
}
}
Expand All @@ -114,7 +112,7 @@ pub fn NewHTTPContext(comptime ssl: bool) type {
if (!comptime ssl) {
@compileError("ssl only");
}
const opts = client.tls_props.?.asUSocketsForClientVerification();
const opts = client.tls_props.?.get().asUSocketsForClientVerification();
try this.initWithOpts(&opts);
}

Expand Down Expand Up @@ -188,7 +186,7 @@ pub fn NewHTTPContext(comptime ssl: bool) type {
/// If `did_have_handshaking_error_while_reject_unauthorized_is_false`
/// is set, then we can only reuse the socket for HTTP Keep Alive if
/// `reject_unauthorized` is set to `false`.
pub fn releaseSocket(this: *@This(), socket: HTTPSocket, did_have_handshaking_error_while_reject_unauthorized_is_false: bool, hostname: []const u8, port: u16, ssl_config: ?*SSLConfig) void {
pub fn releaseSocket(this: *@This(), socket: HTTPSocket, did_have_handshaking_error_while_reject_unauthorized_is_false: bool, hostname: []const u8, port: u16, ssl_config: ?SSLConfig.SharedPtr) void {
// log("releaseSocket(0x{f})", .{bun.fmt.hexIntUpper(@intFromPtr(socket.socket))});

if (comptime Environment.allow_assert) {
Expand All @@ -214,11 +212,9 @@ pub fn NewHTTPContext(comptime ssl: bool) type {
pending.hostname_len = @as(u8, @truncate(hostname.len));
pending.port = port;
pending.owner = this;
// Hold a ref on ssl_config while it's in the keepalive pool
pending.ssl_config = ssl_config;
if (ssl_config) |config| {
config.ref();
}
// Clone a strong ref for the keepalive pool; the caller retains
// its own ref via HTTPClient.tls_props.
pending.ssl_config = if (ssl_config) |s| s.clone() else null;

log("Keep-Alive release {s}:{d}", .{
hostname,
Expand Down Expand Up @@ -332,11 +328,8 @@ pub fn NewHTTPContext(comptime ssl: bool) type {
}

fn addMemoryBackToPool(pooled: *PooledSocket) void {
// Release the ssl_config ref held by this pooled socket
if (pooled.ssl_config) |config| {
config.deref();
pooled.ssl_config = null;
}
if (pooled.ssl_config) |*s| s.deinit();
pooled.ssl_config = null;
assert(pooled.owner.pending_sockets.put(pooled));
}

Expand Down Expand Up @@ -443,7 +436,7 @@ pub fn NewHTTPContext(comptime ssl: bool) type {
}

// Match ssl_config by pointer equality (interned configs)
if (socket.ssl_config != ssl_config) {
if (SSLConfig.rawPtr(socket.ssl_config) != ssl_config) {
continue;
}

Expand All @@ -464,11 +457,9 @@ pub fn NewHTTPContext(comptime ssl: bool) type {
continue;
}

// Release the pooled socket's ssl_config ref (caller has its own ref)
if (socket.ssl_config) |config| {
config.deref();
socket.ssl_config = null;
}
// Release the pool's strong ref (caller has its own via tls_props)
if (socket.ssl_config) |*s| s.deinit();
socket.ssl_config = null;
assert(this.pending_sockets.put(socket));
log("+ Keep-Alive reuse {s}:{d}", .{ hostname, port });
return http_socket;
Expand Down Expand Up @@ -500,7 +491,7 @@ pub fn NewHTTPContext(comptime ssl: bool) type {
client.connected_url.hostname = hostname;

if (client.isKeepAlivePossible()) {
if (this.existingSocket(client.flags.reject_unauthorized, hostname, port, client.tls_props)) |sock| {
if (this.existingSocket(client.flags.reject_unauthorized, hostname, port, SSLConfig.rawPtr(client.tls_props))) |sock| {
if (sock.ext(**anyopaque)) |ctx| {
ctx.* = bun.cast(**anyopaque, ActiveSocket.init(client).ptr());
}
Expand Down
Loading