Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
15 changes: 13 additions & 2 deletions packages/bun-usockets/src/crypto/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ static int us_sni_ex_idx = -1;
static int us_ctx_cache_ex_idx = -1;
/* Marks an SSL_CTX whose verification store holds user-provided CAs (the
* ca/caFile options or a later addCACert): the per-socket client attach must
* not replace such a store with the process-shared default roots. */
* not replace such a store with the process-shared default roots, and
* tls.setDefaultCACertificates() must not override it either. */
static int us_ctx_user_ca_ex_idx = -1;
static int us_ssl_reneg_state_idx = -1;
/* Per-connection async-SNI suspension state (select_certificate_cb retry). */
Expand Down Expand Up @@ -1164,9 +1165,9 @@ void us_internal_ssl_attach(struct us_socket_t *s, SSL_CTX *ctx,
* bundle without touching the CTX (servers using the same CTX never pay
* the ~150-root build). us_verify_callback returns 1 so the handshake
* never aborts here — JS reads verify_error and decides. */
us_ex_idx_ensure();
if (SSL_CTX_get_verify_mode(ctx) == SSL_VERIFY_NONE) {
SSL_set_verify(ssl, SSL_VERIFY_PEER, us_verify_callback);
us_ex_idx_ensure();
if (!SSL_CTX_get_ex_data(ctx, us_ctx_user_ca_ex_idx)) {
/* Default context: give this socket the process-shared root bundle.
* A context whose store holds user-provided CAs (ca/caFile options or
Expand All @@ -1175,6 +1176,16 @@ void us_internal_ssl_attach(struct us_socket_t *s, SSL_CTX *ctx,
X509_STORE *roots = us_get_shared_default_ca_store();
if (roots) SSL_set0_verify_cert_store(ssl, roots);
}
} else if (us_has_user_root_certs()
&& !SSL_CTX_get_ex_data(ctx, us_ctx_user_ca_ex_idx)) {
/* CTX was built against the process defaults (request_cert without an
* explicit `ca`), but tls.setDefaultCACertificates() has since replaced
* those defaults. Override the verify store for this SSL only so the
* new roots take effect without rebuilding the cached SSL_CTX
* (fetch()/https.request() cache their HTTPS context for the process
* lifetime). CTXs with user CAs are left alone. */
X509_STORE *roots = us_get_shared_default_ca_store();
if (roots) SSL_set0_verify_cert_store(ssl, roots);
}
} else {
SSL_set_accept_state(ssl);
Expand Down
114 changes: 102 additions & 12 deletions packages/bun-usockets/src/crypto/root_certs.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "./root_certs.h"
#include "./root_certs_header.h"
#include "./internal/internal.h"
#include <atomic>
#include <mutex>
#include <string.h>
#include "./default_ciphers.h"
Expand Down Expand Up @@ -187,6 +188,67 @@
return us_get_default_ca_certificates()->root_extra_cert_instances;
}

// ---------------------------------------------------------------------------
// User-overridden default CA certificates (tls.setDefaultCACertificates)
//
// Node.js lets JS replace the default trust root set at runtime. Once set,
// *every* consumer of the "default" store — us_get_default_ca_store() and
// us_get_shared_default_ca_store() — must ignore the bundled/system/extra
// sources and build the store purely from this user-supplied set. The
// override is process-global here (Node.js uses thread_local so Workers are
// isolated; we accept the simpler process-wide semantics for now) and guarded
// by shared_store_mutex because the shared store can be read from socket
// I/O paths on other threads while JS swaps it.
// ---------------------------------------------------------------------------
static std::mutex shared_store_mutex;
static X509_STORE *shared_store = nullptr;
static STACK_OF(X509) *user_root_certs = nullptr;
// Atomic so the lock-free fast-path check in us_has_user_root_certs()
// (called per-SSL from us_internal_ssl_attach) is well-defined when a
// Worker is concurrently setting the override. Relaxed is enough: a
// stale false falls through to the shared-store path (which also honours
// the override under the mutex), a stale true just costs one extra
// SSL_set0_verify_cert_store.
static std::atomic<bool> has_user_root_certs { false };

extern "C" int us_has_user_root_certs() {
return has_user_root_certs.load(std::memory_order_relaxed) ? 1 : 0;
}

extern "C" void us_set_user_root_certs(STACK_OF(X509) *certs) {
std::lock_guard<std::mutex> lock(shared_store_mutex);
if (user_root_certs) {
sk_X509_pop_free(user_root_certs, X509_free);
}
user_root_certs = certs; // may be nullptr for an explicit empty set
has_user_root_certs = true;

// Drop the cached shared store so the next consumer rebuilds from the
// override. Existing SSL*s already hold their own X509_STORE reference via
// SSL_set0_verify_cert_store, so this only affects new connections.
if (shared_store) {
X509_STORE_free(shared_store);
shared_store = nullptr;
}

Check failure on line 232 in packages/bun-usockets/src/crypto/root_certs.cpp

View check run for this annotation

Claude / Claude Code Review

addCACert clone-on-write check defeated by shared_store invalidation

Nulling `shared_store` here means `us_get_shared_default_ca_store()` returns a *new* `X509_STORE*` on the next call, but `us_ssl_ctx_add_ca_cert()` (openssl.c:959-960) still relies on pointer identity (`store == us_get_shared_default_ca_store()`) to decide whether to clone-on-write. An SSL_CTX built via the `request_cert`-without-`ca` branch (openssl.c:881) before `setDefaultCACertificates()` still holds the old shared store v1; after the override, `addCACert` compares v1 against v2, the `store_
Comment thread
robobun marked this conversation as resolved.
}

STACK_OF(X509) *us_dup_user_root_certs(bool *out_has_override) {
// Hand back an owned, up-ref'd snapshot so the caller can serialise the
// certs to PEM without racing us_set_user_root_certs() on another Worker.
// *out_has_override distinguishes "no override installed" from "empty
// override installed" (both return nullptr). Caller frees the returned
// stack via sk_X509_pop_free(.., X509_free).
std::lock_guard<std::mutex> lock(shared_store_mutex);
if (out_has_override) *out_has_override = has_user_root_certs;
if (user_root_certs == nullptr) return nullptr;
STACK_OF(X509) *dup = sk_X509_dup(user_root_certs);
if (dup == nullptr) return nullptr;
for (size_t i = 0; i < sk_X509_num(dup); i++) {
X509_up_ref(sk_X509_value(dup, i));
}
return dup;
}

// Single source of truth for the OS trust store. Loaded on first demand,
// independent of --use-system-ca / NODE_USE_SYSTEM_CA, so that
// tls.getCACertificates('system') matches Node.js (which always reads the
Expand All @@ -207,12 +269,28 @@
return system_certs;
}

extern "C" X509_STORE *us_get_default_ca_store() {
static X509_STORE *us_build_default_ca_store_locked() {
X509_STORE *store = X509_STORE_new();
if (store == NULL) {
return NULL;
}

// If JS overrode the defaults via tls.setDefaultCACertificates(), honour
// that exclusively — Node.js does not merge bundled/system/extra back in.
// X509_STORE_add_cert() takes its own reference, so no up_ref here —
// unlike the bundled/extra/system blocks below (whose certs are
// process-lifetime statics so the extra ref is harmless), user_root_certs
// is freed on every subsequent setDefaultCACertificates() and an extra
// ref would leak.
if (has_user_root_certs) {
if (user_root_certs) {
for (int i = 0; i < (int)sk_X509_num(user_root_certs); i++) {
X509_STORE_add_cert(store, sk_X509_value(user_root_certs, i));
}
Comment thread
robobun marked this conversation as resolved.
}
return store;
}

if (!X509_STORE_set_default_paths(store)) {
X509_STORE_free(store);
return NULL;
Expand Down Expand Up @@ -253,18 +331,30 @@
return store;
}

// Process-wide immutable default store. Safe to share across SSL_CTXs that
// don't add per-config CAs (the user-`ca` path in build_raw populates the
// SSL_CTX's own private, initially-empty store instead). This makes the
// ~150-root build a once-per-process cost instead of once-per-SSL_CTX, which
// is what kept Bun.connect({tls:true}) under the node-tls-server.test.ts
// 100ms cold-path budget in debug+ASAN.
extern "C" X509_STORE *us_get_default_ca_store() {
// Serialise with us_set_user_root_certs() so a Worker swapping the
// override can't race another Worker building a per-config store.
std::lock_guard<std::mutex> lock(shared_store_mutex);
return us_build_default_ca_store_locked();
}

// Process-wide default store cached behind a mutex. Safe to share across
// SSL_CTXs that don't add per-config CAs (the user-`ca` path in build_raw
// populates the SSL_CTX's own private, initially-empty store instead). This
// makes the ~150-root build a once-per-process cost instead of
// once-per-SSL_CTX, which is what kept Bun.connect({tls:true}) under the
// node-tls-server.test.ts 100ms cold-path budget in debug+ASAN.
//
// Not std::call_once: tls.setDefaultCACertificates() must be able to
// invalidate the cached store so subsequent connections see the override.
// us_set_user_root_certs() takes the same mutex and nulls shared_store.
extern "C" X509_STORE *us_get_shared_default_ca_store() {
static X509_STORE *shared = nullptr;
static std::once_flag once;
std::call_once(once, []() { shared = us_get_default_ca_store(); });
if (shared) X509_STORE_up_ref(shared);
return shared;
std::lock_guard<std::mutex> lock(shared_store_mutex);
if (shared_store == nullptr) {
shared_store = us_build_default_ca_store_locked();
}
if (shared_store) X509_STORE_up_ref(shared_store);
return shared_store;
}

extern "C" const char *us_get_default_ciphers() {
Expand Down
3 changes: 3 additions & 0 deletions packages/bun-usockets/src/crypto/root_certs_header.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@

STACK_OF(X509) *us_get_root_extra_cert_instances();
STACK_OF(X509) *us_get_root_system_cert_instances();
STACK_OF(X509) *us_dup_user_root_certs(bool *out_has_override);

#else
#define CPPDECL extern
#endif

CPPDECL X509_STORE *us_get_default_ca_store();
CPPDECL X509_STORE *us_get_shared_default_ca_store();
CPPDECL void us_set_user_root_certs(STACK_OF(X509) *certs);
CPPDECL int us_has_user_root_certs();
Loading
Loading