Skip to content

install: build the --ca / --cafile SSL_CTX on the main thread - #46399

Open
robobun wants to merge 1 commit into
mainfrom
robobun/29e76d19/cafile-init-error-main-thread
Open

robobun wants to merge 1 commit into
mainfrom
robobun/29e76d19/cafile-init-error-main-thread

Conversation

@robobun

@robobun robobun commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator

Related to #16103

Problem

  • A ca / cafile that does not load makes a command without network exit 0 or 1 by a race. bun install (no dependencies) exits 1 with HTTPThread: could not find CA file: '...' in 28 of 200 runs. bun pm pkg set left package.json empty in 1 of 400.
  • The HTTP thread ends the process through InitOpts.on_init_error (src/http/HTTPThread.rs:1335).

Fix

  • PackageManager::init builds the SSL_CTX on the main thread (InitOpts::with_ca) and hands it to the HTTP thread. A bad CA exits 1 there with the same message, before the command prints or writes.
  • That build is the only step a CA can fail, and it needs no HTTP thread state.
  • Verified: test/cli/install/bun-install-registry.test.ts (10 new tests, 9 fail on main), config-precedence.test.ts, escape-control-characters.test.ts.
  • Self-reviewed: 17 concerns raised, 16 addressed. Not done: the Windows %SystemRoot% move (Notes).

Background

  • The HTTP thread runs the requests of bun install. Only PackageManager::init passes CA options.
  • An SSL_CTX is the TLS configuration. A user CA replaces its default trust store.
  • Considered: init waits for the HTTP thread's verdict (keeps the callback), or a report at the first request (a bad CA stays silent).

Downsides

  • Behaviour change: bun pm bin with a bad CA exits 0 in 99.3% of runs on main, now never. npm ignores a missing cafile (Missing cafile to be a warning instead of error #16103): a maintainer must decide.
  • Cost with a valid CA: +0.26 ms per process (1 certificate), +3.26 ms (151). No CA: +0.08 ms, noise 0.07 ms.
  • Two exits stay on the HTTP thread: src/http/HTTPThread.rs:335 (default trust store), :1248 (Windows %SystemRoot%).
Notes

All numbers are from release builds of main (9b4284f) and of this branch on Linux x64 with 12 cores. The measured branch build is e24c6b1b5. The commits after it change comments, docs and tests only.

Question for a maintainer

  • Should a CA that does not load stop a command that makes no request?
  • This PR answers yes. It is the intent in the comment that this PR replaces (src/http/HTTPThread.rs:1329-1333 on main: "validate now so a bad CA file fails the process at thread start"). The existing tests already pin it for every command that makes a request, also with a plain http:// registry.
  • The other answer has support too. Missing cafile to be a warning instead of error #16103 asks for a warning when cafile does not exist. npm 11.16.0 ignores such a file. install: fix regressions since 1.4.3 and resolve peer dependencies more like pnpm #45898 made an unreadable .npmrc or bunfig.toml non-fatal, "like npm does".
  • What changes for users is narrow. Commands that use the network fail on main in every run. In a project with 40 workspace packages, a no-op bun install with a missing cafile already exits 1 in 364 of 400 runs on main. The commands that now fail and mostly passed before are the fast local ones: on main bun pm bin exits 0 in 99.3% of runs, bun pm pkg get in 98.3%, bun remove in 96.1%, bun install in an empty project in 86.4%.
  • Either answer needs this change, because both need the verdict on the main thread. With this PR, "warn and continue" is one match arm in PackageManager::init, plus the missing-cafile rows of the new tests and the two docs paragraphs. That answer must also say which trust store such a run uses: in bun a user CA replaces the default trust store.

The race on main

  • Five bad CA settings (missing cafile, cafile with garbage PEM, empty cafile, cafile that is a directory, --ca not-valid) times eight commands that need no network, in an empty project, 400 runs per cell.
  • main: 1 of 40 cells gives one outcome, and a cell has up to 8. 13,102 of 16,000 runs exit 0 (81.9%). 32 runs exit 0 with the error printed. 8 runs of pm bin / pm pkg get print their result and then exit 1.
  • main, exit 0 per command: install 86.4%, install --dry-run 89.0%, install --offline 92.4%, link 91.0%, pm bin 99.3%, pm pkg get name 98.3%, remove foo 96.1%, pm pack --dry-run 2.6%.
  • This branch: 40 of 40 cells give one outcome. All 16,000 runs exit 1 with an empty stdout and only the message on stderr.
  • Under gdb, bun install --cafile <missing> on main ends with exit_group(1) on the HTTP Client thread in 3 of 5 runs and with exit_group(0) on the main thread in 2 of 5. On this branch: exit_group(1) on the main thread in 5 of 5.

Damage on main beyond the exit code (400 runs each, --cafile of a file that does not exist)

Command main this branch
bun pm pkg set description=x 392 exit 0 with the key written, 3 exit 1 with the key written, 1 exit 1 with an empty package.json, 4 exit 1 unchanged 400 exit 1, package.json unchanged
no-op bun install, 40 workspace packages 364 exit 1, 36 exit 0 400 exit 1
bun install into a new node_modules, 40 workspace packages 314 exit 1 with nothing placed, 65 exit 1 with all 40 placed, 5 exit 1 with a partial node_modules, 16 exit 0 400 exit 1, nothing placed

Cost

  • bun pm bin in a project with no dependencies, 300 runs per arm, interleaved main / branch / main, wall time with process start:
CA main (two arms) branch delta
none 2.74 ms, 2.81 ms 2.86 ms +0.08 ms (the two main arms differ by 0.07 ms, IQR 0.6 ms)
--cafile, 1 certificate 2.71 ms, 2.70 ms 2.97 ms +0.26 ms
--cafile, 151 certificates (226 KB) 2.77 ms, 2.83 ms 6.05 ms +3.26 ms
  • On main the HTTP thread does this build beside the main thread, and a fast command exits before the build ends. So for such a command the build is new work. Under gdb, bun pm bin with a valid cafile exits on main before any SSL_CTX build in 5 of 5 runs.
  • The 1-certificate number is mostly the first use of BoringSSL in the process. The build of that SSL_CTX alone takes 23 to 30 µs, and 2.5 to 2.9 ms for the 151 certificates (tls.createSecureContext({ caFile }) in a loop).
  • The loader reads and parses a cafile twice (SSL_load_client_CA_file, then SSL_CTX_load_verify_locations). This PR does not change the loader.
  • The number of builds per process does not change: 1 SSL_CTX build and 2 opens of the cafile with a valid CA, on the main thread in place of the HTTP Client thread (gdb breakpoint on us_ssl_ctx_build_raw, catch syscall openat). With no CA: 0 builds on both.
  • Main-thread syscalls of bun pm bin: no CA 142 on both. Valid cafile: 142 on main, 156 (1 certificate) and 271 (151 certificates) on this branch. Futex waits on the main thread: 0 on both.
  • No CA, bun install with no dependencies: 167 to 170 main-thread syscalls on both builds (12 runs each).
  • No CA, per fetch() call: the http_thread::init(&Default::default()) site in FetchTasklet::queue goes from 11 instructions (6 stores) to 4 (1 store). size_of::<InitOpts>() goes from 48 to 8 bytes.
  • Per HTTPS connect: the first-use test of the default context is cmp + je (main: load, store, cmp, jne). One branch on both.
  • Size of bun-profile: .text 81,904,485 -> 81,901,669 bytes (-2,816), .data -64, .bss -64 (llvm-size -A). The stripped bun has the same file size.

Design

  • InitOpts now holds only default_ssl_ctx: Option<OwnedSslCtx>. The one diverging function pointer type in src/ (fn(InitError, &InitOpts) -> !) is gone, with both of its call sites.
  • init_once stores the context in the HTTP_THREAD static before it spawns the thread, and on_start moves it into https_context. No closure carries it, so unsafe impl Send for InitOpts is deleted and none is added.
  • HttpThread.lazy_https_init is gone: the first-use test reads https_context.secure.
  • Other designs weighed:
    • http_thread::init waits for the HTTP thread's verdict and runs the callback on the calling thread. It keeps the diverging callback type and its first-use call site, and the main thread waits for the thread start as well as the build.
    • A report at the first request. It costs a valid CA nothing. But a bad CA stays silent for every command without network, each sender needs a gate, and the failure moves into the install pass.
    • A check on the main thread only, with the HTTP thread build as it is. A valid CA is then built twice, and the exit on the HTTP thread stays.

Tests

  • New: four bad CA settings times bun install and bun pm bin in a project with no dependencies. Each expects an empty stdout, only the message on stderr and exit 1. One test checks that bun pm pkg set leaves package.json byte for byte as it was. One runs bun pm bin with a --cafile that loads.
  • Of these 10 tests, 9 fail on a release build of main in each of three runs. On a debug build of main the 8 bad-CA rows fail (8 of 8).
  • The tests of a relative cafile near the path buffer length no longer pass --silent. Its only purpose was to keep a line of the main thread out of a message that another thread printed.
  • config-precedence.test.ts now expects the message as the whole of stderr for a ~/ cafile.
  • Ran on a debug (ASAN) build: all of bun-install-registry.test.ts (498 pass before the last test edits, then its "certificate authority" block again: 32 pass), config-precedence.test.ts and escape-control-characters.test.ts (207 pass), the ca / cafile tests of npmrc.test.ts, bun-install.test.ts and bun-pm-diff.test.ts, and fetch.tls.test.ts.
  • fetch.tls.test.ts has one failure that main has too in this container (trusts a self-issued certificate without keyCertSign ...).
  • With CI's sanitizer options (detect_leaks=1), 480 runs of bun pm bin / bun install with a valid CA, a bad CA and no CA gave no sanitizer report.
  • cargo check of bun_http and bun_install passes for x86_64-pc-windows-msvc, aarch64-apple-darwin and aarch64-unknown-linux-gnu. Not run: the tests on macOS and Windows.

Docs

  • docs/runtime/bunfig.mdx and docs/pm/npmrc.mdx now say when bun loads the certificate and what a CA that does not load does. Each sentence was run on this branch:
    • bun x <name> with a missing cafile in ~/.bunfig.toml exits 0 when the binary is in node_modules/.bin, and exits 1 with the message when it must install.
    • Auto-install does not apply ca or cafile, on main or on this branch. With a local HTTPS registry and its certificate in cafile, bun add installs, and bun --install=force x.js fails with DEPTH_ZERO_SELF_SIGNED_CERT.
    • A ca that holds a PEM key and no certificate is accepted. An empty ca is not.
  • npm 11.16.0 with no dependencies: a missing cafile, a cafile with garbage PEM, an empty cafile and ca=not-valid all exit 0. A cafile that is a directory exits 235.

Not covered, and overlap

The HTTP thread built the default HTTPS context from the CA options of
PackageManager::init and ended the process itself when a CA did not
load. The main thread did not wait for that result, so a command that
needs no network ended with exit 0 or exit 1 by a race.

PackageManager::init now builds the SSL_CTX itself, reports a CA that
does not load, and hands the finished context to the HTTP thread.
InitOpts carries that context in place of the CA strings and the
diverging on_init_error callback.
@coderabbitai

coderabbitai Bot commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 10 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 4d393eec-2963-4c36-b618-51cff5ecaec4
📥 Commits

Reviewing files that changed from the base of the PR and between 491aaad and abbc2db.

📒 Files selected for processing: 7
  • docs/pm/npmrc.mdx
  • docs/runtime/bunfig.mdx
  • src/http/HTTPContext.rs
  • src/http/HTTPThread.rs
  • src/install/PackageManager.rs
  • test/cli/install/bun-install-registry.test.ts
  • test/cli/install/config-precedence.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Status

Reproduced on release builds of 1.4.3-canary.1+367d939d9 and of main (9b4284f), Linux x64:

mkdir /tmp/x && cd /tmp/x
printf '[install]\ncafile = "path/to/cafile"\n' > bunfig.toml
echo '{ "name": "x", "version": "0.0.0" }' > package.json
for i in $(seq 1 200); do
  HOME=/tmp/x BUN_INSTALL_CACHE_DIR=/tmp/x/.cache bun install > out.txt 2>&1 || echo "iter $i: exit 1"
  rm -rf node_modules bun.lock .cache
done

28 of 200 runs exit 1 with HTTPThread: could not find CA file: '/tmp/x/path/to/cafile'. The other runs exit 0. With this PR every run exits 1 with that message.

PR: #46399

@robobun

robobun commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:05 PM PT - Oct 21st, 2026

❌ @robobun, your commit abbc2db has 1 failures in Build #130382 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 46399

That installs a local version of the PR into your bun-46399 executable, so you can run:

bun-46399 --bun

@robobun

robobun commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

@Jarred-Sumner @dylan-conway One decision in this PR needs a maintainer.

Should a ca / cafile that does not load stop a command that makes no request, such as bun pm bin?

  • This PR answers yes. The command prints the existing message and exits 1 before it starts. On main such a command exits 0 in most runs, by a race with the HTTP thread (bun pm bin: 99.3% of 2,000 runs). Commands that make a request fail on main in every run, and the existing tests pin that.
  • Missing cafile to be a warning instead of error #16103 asks for a warning when cafile does not exist, as npm does. install: fix regressions since 1.4.3 and resolve peer dependencies more like pnpm #45898 made an unreadable .npmrc or bunfig.toml non-fatal for the same reason.
  • If the answer is "warn and continue", the change is one match arm in PackageManager::init, the missing-cafile rows of the new tests and the two docs paragraphs. The build of the SSL_CTX on the main thread stays for both answers. That answer must also say which trust store such a run uses, because a user CA replaces the default trust store.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline finding, I also checked the cross-thread SSL_CTX handoff: init_once gives the HttpThread its own SSL_CTX_up_ref clone before the spawn, so the main thread dropping http_thread_opts after init returns only releases its own reference; and the is_none() guard in ensure_https_context_init cannot re-enter since init_with_ssl_ctx sets secure unconditionally and runs only on the HTTP thread.

Extended reasoning...

The change moves user-CA SSL_CTX construction from the HTTP thread to PackageManager::init on the main thread, replacing raw pointer InitOpts and the diverging on_init_error callback with an Option; it touches TLS trust-store setup for bun install. The posted inline finding covers the remaining HTTP-thread exit path on Windows; the refcount handoff and lazy-init guard were examined and hold.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟣 src/http/HTTPThread.rs — Windows users whose environment lacks %SystemRoot% still get the nondeterministic exit this PR removes for CA errors: bun pm pkg set can leave package.json empty or written with exit 0 or 1 by a race. The check at src/http/HTTPThread.rs:1243 runs on the HTTP thread after spawn and calls Global::crash() at :1248 while the main thread keeps writing files; the PR moved only the CA exit to the main thread. Fix: perform every process-ending check that depends only on environment or config on the calling thread before http_thread::init spawns, so no sibling site keeps the HTTP-thread exit race.

    Why this was flagged

    On Windows, a process launched with a sanitized environment (CI runners, spawn with a custom env that omits SystemRoot, container images) runs bun pm pkg set description=x or bun install. on_start at src/http/HTTPThread.rs:1205 runs on the spawned HTTP thread; getenv_w("SystemRoot") at :1243 returns None and Global::crash() at :1248 calls exit_group(1) from that thread while the main thread is in the middle of writing package.json or placing node_modules. This is exactly the mechanism the PR documents for CA errors (392/400 exit 0 with the key written, 1/400 empty package.json on base) and leaves it in place for this sibling input. The check depends only on an environment variable and could run on the main thread before the spawn at src/http/HTTPThread.rs:1177, the same way InitOpts::with_ca now does for CA; REVIEW.md asks for the whole class to be fixed in the same PR. Remedy: hoist the %SystemRoot% check into init_once on the calling thread before the spawn.

    Verification: The PR description says "Not done: the Windows %SystemRoot% move". Trigger: Windows process lacking SystemRoot running any install/pm command. On getenv_w("SystemRoot") returning None, on_start() on the spawned HTTP thread calls bun_core::Global::crash() (src/http/HTTPThread.rs:1248), so the exit races whatever the main thread is writing. The base already fails by this exact route; merging makes nothing worse.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant