Repository navigation
Bun global + crypto: remove unsafe from BunObject.rs and runtime/crypto - #40399
Jarred-Sumner wants to merge 20 commits into
Conversation
|
Updated 11:23 AM PT - Sep 8th, 2026
❌ @Jarred-Sumner, your commit 1665544 has 1 failures in 🧪 To try this PR locally: bunx bun-pr 40399That installs a local version of the PR into your bun-40399 --bun |
There was a problem hiding this comment.
Thanks for the fix in d585edb — the inline-mod closing-brace matcher now handles trailing comments. This automated pass found no further issues.
Given the scope (59 files; server lifecycle moved to RefPtr/self_ref/teardown(), WeakPtr gaining Drop+Clone, RequestContext self-owning its pool slot, NodeHTTPResponse::create moved Rust-side with a changed C++ ABI, the new HotServers registry) and that it's stacked on #40204/#40214, a human pass is still warranted before merge.
What was reviewed: the new DigestCtx/HmacCtx bounds-check output before EVP_DigestFinal_ex/HMAC_Final (fixes the pre-existing min(len, size) out-param footgun the description notes); create_uint8_array_from_vec — non-empty branch hands the mimalloc allocation to MarkedArrayBuffer_deallocator, empty branch drops before returning; err_reason_error_string copies the strerror buffer for ERR_LIB_SYS before returning; the serve_with! error paths now server.deref() on both post-init exception exits.
Extended reasoning...
Overview
This PR continues the zero-unsafe programme across BunObject.rs (35→0) and runtime/crypto/ (all files → 0). It introduces owned BoringSSL context wrappers (DigestCtx, HmacCtx) in a new bun_boringssl_sys::digest module, converts EVP_*() getters to return &'static EVP_MD, and threads Option<&ENGINE> everywhere raw engine pointers used to flow. On the BunObject side, every Bun.* callback/lazy-property is migrated to the HOST_EXPORT codegen (which learned inline-mod tracking and lifetime-only generics), and several ownership models are reworked: Resolver::custom_dir_paths becomes Option<Box<[BunString]>>, EditorContext.name becomes Vec<u8>, mmap gets an owning MappedFile handed to a new uint8_array_from_mapped_file, and the --hot server registry becomes a typed HotServers map stored via dyn Any in RareData.
The largest surface area is the runtime/server/ rework carried in from #40214: NewServer becomes CellRefCounted with a self_ref held while the uWS app exists; init() returns RefPtr<Self>; listen()/teardown() replace the raw-pointer + deinit() model; RequestContext stores its own Pooled slot token so deinit drops it rather than calling back through the server; WeakPtr gains Drop/Clone and interior-mutable WeakPtrData; AnyServer is rewritten as a BackRef-based enum; and NodeHTTPResponse::create moves from C++ to Rust with a corresponding signature change to NodeHTTPServer__onRequest_{http,https} (drops any_server/upgrade_ctx/out-param, adds nodeResponseObject/hasBody).
Security risks
Crypto changes are wrapper-shaped — the underlying BoringSSL calls are unchanged, and the new DigestCtx::final_/HmacCtx::final_/digest() add explicit output-length guards that the old code lacked. The err_reason_error_string change correctly copies the strerror() buffer for ERR_LIB_SYS rather than returning a 'static borrow of thread-mutable storage. No new untrusted-input parsing. The mmap deallocator's page-rounding logic is preserved from the old inline version. I did not identify security-relevant regressions.
Level of scrutiny
High. The server-lifecycle refcounting rework is the kind of change where a missed deref() on one exit path leaks a server for the process lifetime, and an extra one is a UAF. The WeakPtr::drop addition means every existing WeakPtr field that was previously manually deref()ed now needs auditing for double-release (I could not verify all sites in the truncated diff). The NodeHTTP.cpp ABI change must be matched exactly on both sides. The RequestContext::pool_slot self-ownership pattern is subtle. This is production-critical hot-path code (Bun.serve, node:http, every hash) and the diff is ~623 KB across 59 files.
Other factors
- Stacked: base is
claude/subprocess-zero-unsafe(#40204) and merges #40214; the description says to retarget once those land, so this cannot merge as-is regardless. - Prior feedback addressed: my earlier nit on
generate-host-exports.tswas fixed in d585edb exactly as suggested. - Testing: the description lists extensive debug+ASAN test coverage and byte-identical crypto verification against Node/system-bun, plus perf measurements showing negligible instruction-count delta.
- No bugs found: the multi-agent bug hunt surfaced nothing this run; the candidate concerns I checked (Vec→Uint8Array ownership transfer, digest output-buffer sizing,
ERR_LIB_SYSstrerror lifetime,serve_with!error-path ref balance) all held up.
Given the scale, the lifecycle/refcount rework, and the stacked-PR status, this needs a maintainer's eyes.
ca74b3f to
d585edb
Compare
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs this pass; both earlier nits (inline-mod brace matcher, stale cached_blob comments) are addressed. Given the scope — 59 files including the NewServer ref-count/lifecycle rework, RequestContext pool-slot ownership, the WeakPtr Drop/Clone addition, and the NodeHTTPServer__onRequest_* ABI change — plus the fact that this is stacked on #40204/#40214, a human look is still worthwhile.
Checked: DigestCtx/HmacCtx final_ short-buffer guards vs. all callers; create_uint8_array_from_vec empty-Vec path and mi_free deallocator pairing; NodeHTTPResponse::create's three initial refs vs. the sync/async release paths in on_node_http_request_with_upgrade_ctx; err_reason_error_string's ERR_LIB_SYS owned-copy for strerror.
Extended reasoning...
Overview
This PR continues the zero-unsafe programme across BunObject.rs (35→0) and runtime/crypto/ (all → 0). It introduces safe wrappers in bun_boringssl_sys::digest (DigestCtx, HmacCtx, one-shot digest/pbkdf2_hmac), converts EVP_*() getters to &'static EVP_MD, adds ArrayBuffer::create_uint8_array_from_vec/uint8_array_from_mapped_file and bun_sys::MappedFile, and moves every BunObject_callback_*/_lazyPropCb_* export onto the HOST_EXPORT codegen path (which learned inline-mod tracking and lifetime-only generics). It also carries the merged server-zero-unsafe work: NewServer becomes CellRefCounted with JsCell/Cell fields, init() returns RefPtr<Self>, listen()/teardown() manage a self_ref, request contexts own their pool slot via Pooled<'static, ..>, AnyServer becomes a BackRef-based enum, WeakPtr gains Cell-based interior mutability plus Drop/Clone, and NodeHTTPResponse creation moves from C++ to Rust with a changed NodeHTTPServer__onRequest_{http,https} signature.
Security risks
No new attack surface. The crypto changes are wrapper-level (BoringSSL calls unchanged) with added bounds checks in DigestCtx::final_/HmacCtx::final_ that fix a pre-existing footgun where EVP::hash/final treated the out-length as an in-bound. The err_reason_error_string change correctly copies strerror() output (ERR_LIB_SYS) rather than returning a 'static borrow of thread-local storage.
Level of scrutiny
High. The server portion rewrites ownership and teardown ordering for NewServer, RequestContext, NodeHTTPResponse, and ServePlugins — all ref-counted, all with GC-visible wrappers, several with re-entrant JS callbacks in their release paths. WeakPtr gaining Drop changes the semantics for every existing holder. The NodeHTTP.cpp ABI change must stay in lockstep with the Rust extern decls. These are exactly the areas REVIEW.md flags as most-blocked (paired acquisition/release, ref-count balance on every terminal path, re-entrance under app.close()).
Other factors
- Stacked on #40204 and merges #40214, so a substantial fraction of the server diff is inherited and presumably reviewed there; the diff shown here is against
claude/subprocess-zero-unsafe, notmain. - Extensive test coverage reported (debug+ASAN across crypto, mmap, zlib/zstd, hot-reload, compile-asset), and
rust-check-allon Windows/macOS. - Both earlier automated-review nits were fixed in follow-up commits and the threads resolved.
- The diff is truncated at ~625K chars;
server_body.rs,uws_sys/,webcore/{Request,Response,Blob}.rs, and theAnyServerdispatch rewrite were not fully visible in the preloaded context, which is another reason human review is appropriate.
30e9a6e to
5f83101
Compare
53ce304 to
42e139e
Compare
There was a problem hiding this comment.
All four points from the last pass are addressed in c5c2e8b — unregister_hot now identity-checks before evicting (with a --hot regression test), create_uint8_array_from_vec uses as_mut_ptr(), arm_on_data reads self.as_ctx_ptr() on both H1 and H3 paths, and the shutdown-gate comment is corrected. Nothing further from this run; given the scope (server core + BoringSSL digest/HMAC wrappers, ~5.5k lines churned), a maintainer pass is still warranted.
What was reviewed: the unregister_hot fix (index lookup + AnyServer PartialEq compare, swap_remove_at only on match); the new hot.test.ts case (bounded WeakRef poll, stdout asserted before exitCode, no fixed sleeps); array_buffer.rs provenance change (mut binding, len read before pointer); arm_on_data(&self) now symmetric with prepare_js_request_context's as_ctx_ptr().
Extended reasoning...
Overview
This PR is a broad zero-unsafe refactor spanning src/runtime/server/ (mod.rs + server_body.rs, ~5.5k lines), the crypto stack (CryptoHasher, EVP, HMAC, PBKDF2, PasswordObject, pwhash, sha_hmac, plus downstream users in s3/mysql/postgres/install), BunObject.rs (all callbacks/lazy-props moved to HOST_EXPORT, typed --hot registry, owned mmap/env/editor state), new bun_boringssl_sys::digest safe wrappers, and supporting changes across uws_sys, jsc, ptr, sys, resolver, and codegen. The latest commit (c5c2e8b) responds to the four inline findings from this bot's previous pass.
Security risks
The change rewrites the FFI surface for BoringSSL digest/HMAC/PBKDF2 primitives and touches password hashing, S3 request signing, MySQL auth, and Postgres SASL. The PR description reports byte-identical output vs. system bun and Node across all CryptoHasher/HMAC/pbkdf2 algorithms, and the wrappers are thin owned-context RAII over the same EVP_* calls, but any regression here is security-relevant. The server refactor changes how request-context pointers are threaded through uWS callbacks — a provenance/lifetime bug there is a UAF vector. No new attack surface is introduced; this is a safety-hardening refactor of existing surface.
Level of scrutiny
High. Per REVIEW.md, memory-safety refactors are the most-blocked category, and this one simultaneously touches crypto FFI, request-context lifetime plumbing, refcount/weak-handle semantics, and a codegen script. The PR is stacked on two other zero-unsafe branches, so the effective diff against main is even larger. This is well outside the "simple, mechanical, or obvious" bar for bot approval.
Other factors
This bot has reviewed three prior pushes; every finding raised (codegen brace matcher, stale cached_blob comments, dead AnyServerTag, custom_dir_paths clone, the unregister_hot eviction race, two provenance nits, and a misleading comment) was addressed within hours, with the eviction race gaining a dedicated regression test. The current run found nothing new and ruled out nothing new beyond verifying those fixes. The test additions follow harness conventions (bounded polling on a WeakRef, Promise.all pipe drain, stdout/stderr asserted before exit code). Deferring so a maintainer signs off on the crypto-wrapper and server-lifetime design; nothing outstanding from the automated side.
5f83101 to
c74f011
Compare
c5c2e8b to
23ce162
Compare
e3d78da to
005bf58
Compare
23ce162 to
e01ea02
Compare
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Additional findings (outside the current diff — GitHub can't attach inline comments there):
-
🟡
src/runtime/server/NodeHTTPResponse.rs— nit: comment onbun_vm_mutstill cites "NodeHTTPResponse__createForJSdisasm" as the perf rationale, but this PR deletes that extern (Rustno_manglefn removed here; C++ decl removed from NodeHTTP.cpp) and replaces it withNodeHTTPResponse::create. Fix: update the comment to nameNodeHTTPResponse::create(or drop the disasm citation) so a reader auditing the hot-path claim isn't sent to a symbol that no longer exists.Extended reasoning...
NodeHTTPResponse__createForJSwas removed on both sides in this diff: the#[unsafe(no_mangle)] extern "C" fn NodeHTTPResponse__createForJSblock in NodeHTTPResponse.rs is deleted and replaced by animpl NodeHTTPResponse { pub(crate) fn create(...) }associated fn, andextern "C" EncodedJSValue NodeHTTPResponse__createForJS(...)plus its call site are deleted from src/jsc/bindings/NodeHTTP.cpp. Grep for the symbol now returns only the comment at NodeHTTPResponse.rs:260 ("NodeHTTPResponse__createForJSdisasm showed thebunVMFFI result dropped on the floor"). Doc-accuracy only; no runtime effect. Per REVIEW.md "Delete dead code in the same PR that makes it dead" and CLAUDE.md's comment-maintenance rule, references to removed identifiers should be updated in the same PR.Verification: nit — The comment at src/runtime/server/NodeHTTPResponse.rs:260 still reads "
NodeHTTPResponse__createForJSdisasm showed thebunVMFFI result dropped on the floor", but this PR deletes that symbol on both sides: the Rust#[unsafe(no_mangle)] pub(crate) unsafe extern "C" fn NodeHTTPResponse__createForJS(...)block is removed and replaced by `impl NodeHTTPResponse { pub(crate) fn… | nit —…
69765fa to
46f59f1
Compare
a2abe1b to
299bd3a
Compare
NewServer is now intrusively refcounted (creator/JS wrapper + a self ref held on behalf of the uWS app registrations) with Cell/JsCell fields and &self / ThisPtr<Self> receivers; AnyServer is an enum of BackRefs; uWS route, filter, listen, client-error and websocket-upgrade registrations go through typed ThisPtr trampolines in bun_uws_sys; RequestContext owns its pool slot; ServePlugins is CellRefCounted with a typed promise ref; the node:http request path passes &AnyServer / &mut Option<RefPtr<NodeHTTPResponse>> through C++ instead of a packed tagged pointer and a raw out-param.
…f a per-request WeakPtr clone PreparedRequestFor carries only the Request's root pointer; the dispatch paths borrow the Request via ctx.request_weakref at each point of use and detach the stack uws request explicitly before deinit/render_missing.
…prep paths hand uws the same pointer
…e refCounted classes
299bd3a to
d5c3494
Compare
23dd8f5 to
b9ae552
Compare
…claude/termcrypto-zero-unsafe # Conflicts: # src/ptr/lib.rs
…P/HMAC/PBKDF2/password to zero unsafe
…t server registry, owned mmap/env/editor state; BunObject.rs to zero unsafe
…ed reason strings, tidy docs
…data reads the root ctx pointer; Unique provenance for the Uint8Array-from-Vec backing store A stopped server's deferred teardown could evict a newer server that re-registered the same id, so the next --hot reload created a third server instead of reloading the live one. unregister_hot now compares the stored AnyServer against self before removing. arm_on_data takes &self and passes as_ctx_ptr() so the H1 and generic (H3) request-prep paths hand uws the same root-provenance pointer. Reword the all-closed-promise shutdown comment to describe the caller-side skip.
d5c3494 to
1665544
Compare
What
Same programme as #40055 … #40385. Stacked on #40204 (base branch
claude/subprocess-zero-unsafe) and merges #40214'sclaude/server-zero-unsafe(needed forNewServer::initreturningRefPtr/AnyServer(BackRef)underBun.serve); retarget to main once those land.src/runtime/api/BunObject.rs35 → 0;src/runtime/crypto/:CryptoHasher.rs16 → 0,HMAC.rs12 → 0,EVP.rs9 → 0,boringssl_jsc.rs/pwhash.rs/PasswordObject.rs/PBKDF2.rs→ 0. (Terminal.rswas already at 0 via #40204.)bun_boringssl_sys::digest—DigestCtx(ownedEVP_MD_CTXwith a digest always installed;final_refuses short buffers),HmacCtx,digest/digest_by_name/md_size/pbkdf2_hmac;EVP_*()getters return&'static EVP_MD; engines areOption<&ENGINE>;err_{lib,func,reason}_error_stringreturnOption<&CStr>/Cow(owned copy forERR_LIB_SYS, whose text isstrerror()).bun_sha_hmac::SHA*::hashis safe (callers in install/sql/exe_format/s3_signing updated).BunObject_callback_*/BunObject_lazyPropCb_*,Bun__CryptoHasherExtern__*,JSPasswordObject__create,Bun__reportErrorareHOST_EXPORTs (generator learned inlinemod {}markers and lifetime-only generics);--hotserver registry is a typedHotServersmap inRareData(servers unregister instop()/teardown());bun_sys::MappedFile(owning mmap) +ArrayBuffer::create_uint8_array_from_vec/uint8_array_from_mapped_file;Resolver::custom_dir_paths,EditorContext.nameowned;StandaloneModuleGraph::embedded_blob(&self, ..)replaces the per-process cachedBlob*created with the first caller's global;Bun__getEnvCount/Keytake the global.Pre-existing issues noticed:
EVP::hash/finalpassedmin(len, size)as if it bounded the write (out-param; now guarded); theStandaloneModuleGraphfirst-globalBlob*cache (replaced);crypto.createHmac("blake2b512")throws "Invalid digest" in Bun though Node supports it (unchanged).Perf (release,
taskset -c 56-63,perf stat -e instructions:u, interleaved ×8, medians):CryptoHasher("sha256").update(1KB)×1e5 +0.26 %;Bun.hash(1KB)×1e6 +0.0 %;crypto.createHmac×1e5 −0.04 %. (First cut was +0.42 % from a 264-byte move inDigestCtx::new; fixed by constructing in place.)Testing
Debug+ASAN,
--timeout 60000: terminal (98), terminal-spawn (16), terminal-platform-gaps (19), inspect (80), mmap (21), password (76), sleep/sleepSync/which, bun-cryptohasher (403), hash (20), cipheriv, x25519, web-crypto (95), web-crypto-sha3, node-crypto (212), crypto.hmac (74), crypto.test (403), pbkdf2 (51), scrypt, lazyhash, oneshot, hmac-algorithm, argon2, extra-memory, zstd (92), zlib (388), open-in-editor-gc, require, module-resolve-filename-paths, node-module-module, cli/hot (12), compile-asset-bunfs (9), regression/31575 — all pass (process.testgetgroupsand serve.test port-1003/react-dom cases are root/sandbox environment). Hand-driven: Terminal spawn/write/resize/close, close-while-alive, child-exit with pending reads, 50 terminals + GC; every CryptoHasher algorithm + HMAC + pbkdf2 byte-identical to system bun and node;Bun.password×20 concurrent + GC; a Worker hashing terminated ×5 — exit 0, no ASAN output. clippy clean on all touched crates;rust-check-allwindows-msvc + aarch64-darwin pass.