From ee92c7fe38b79fc05cf88f7dacd092350d6c1522 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Wed, 8 Jul 2026 23:40:43 -0700 Subject: [PATCH 1/2] Add ForeignRef and make FetchHeaders the owned handle A C++-allocated object that Rust releases through an FFI destructor needs two types: the borrowed opaque (the `opaque_ffi!` ZST) and an owner whose Drop gives the ref back. Four such owners were hand-rolled and 27 more C++ types release by hand at every call site. Add the generic pair to `bun_opaque`: - `ForeignOwned`: this opaque type has a release extern - `ForeignRef`: `Deref` + `Drop`, `repr(transparent)` over `NonNull` - `foreign_owned!(T, release_fn)` emits the impl `FetchHeaders` is the first user, and the owned handle takes the public name; the raw ZST moves into an extern-only `sys` module. - Every receiver is `&self`. The type is `UnsafeCell`-backed precisely so that `&T` carries no `noalias`, and C++ mutates the header storage through the same pointer, so `&mut self` asserted an exclusivity that was never true and never needed. - Constructors return `Self`; C++ always hands back a fresh +1. - `cast()` returns `ManuallyDrop`: it borrows the ref that the JS `Headers` wrapper owns, so releasing it must not be expressible. - The four `void*`-taking externs become `unsafe fn`. C++ dereferences the pointer, and safe Rust can forge a `*mut c_void`, so `safe fn` was unsound on all four. This matches the rule already written above the extern block. - `create()` and `copy_to()` take slices instead of `*mut StringPointer` plus a separate length. - `FetchHeaders::from` and `create_value` had no callers; deleted. The `_`-suffixed raw variants had no external callers; now private. Making `cast()` a `ManuallyDrop` stopped `server.fetch(url, { headers })` from compiling, which is how it turned out to be a use-after-free. That path adopted the ref owned by the JS `Headers` wrapper: `cast_` never bumps the refcount, and the wrapper's `Ref<>` derefs on finalize, so the internal `Request` and the wrapper each released the same single ref. ASan confirms the heap-use-after-free, and the aliasing half is deterministic without it: a header the handler sets on `req.headers` shows up on the caller's `Headers` object. Copy with `clone_this` instead, which is the path `new Response(_, { headers })` already takes. All six `cast`/`cast_` callers were audited; this was the only one that adopted the borrow. Regression test added. The rest of the diff converts the `unsafe { &mut *ptr }` pattern this grew out of: raw-pointer fields become the in-tree owners (`JsCell`, `Cell`, `ParentRef`, `BackRef`, `Box` receivers) across the runtime, and the receivers that only ever needed `&self` say so. No allocation, lock, `Rc`, `Arc`, or refcount is added anywhere in the diff. --- Cargo.lock | 2 + src/ast/e.rs | 11 +- src/bun_alloc/BufferFallbackAllocator.rs | 38 +- src/bun_alloc/lib.rs | 69 +- src/bundler/BundleThread.rs | 3 +- src/bundler/LinkerContext.rs | 144 ++-- src/bundler/ParseTask.rs | 4 +- src/bundler/ServerComponentParseTask.rs | 4 +- src/bundler/ThreadPool.rs | 32 +- src/bundler/bundle_v2.rs | 250 +++--- src/bundler/linker_context/computeChunks.rs | 50 +- .../linker_context/convertStmtsForChunk.rs | 2 +- .../findImportedFilesInCSSOrder.rs | 6 +- .../generateChunksInParallel.rs | 2 +- .../generateCodeForFileInChunkJS.rs | 2 +- .../generateCodeForLazyExport.rs | 7 +- .../generateCompileResultForJSChunk.rs | 32 +- .../linker_context/postProcessCSSChunk.rs | 4 +- .../linker_context/postProcessHTMLChunk.rs | 4 +- .../linker_context/postProcessJSChunk.rs | 4 +- .../linker_context/prepareCssAstsForChunk.rs | 58 +- .../linker_context/writeOutputFilesToDisk.rs | 23 +- src/bundler/transpiler.rs | 42 +- src/bunfig/Cargo.toml | 1 + src/bunfig/arguments.rs | 9 +- src/collections/pool.rs | 8 +- src/crash_handler/lib.rs | 26 +- src/event_loop/AnyEventLoop.rs | 15 +- src/event_loop/MiniEventLoop.rs | 68 +- src/http/AsyncHTTP.rs | 63 +- src/http/HTTPThread.rs | 39 +- src/http/h3_client/ClientSession.rs | 24 +- src/http/h3_client/PendingConnect.rs | 6 +- src/http/h3_client/Stream.rs | 8 +- src/http/h3_client/callbacks.rs | 8 +- src/http_jsc/headers_jsc.rs | 104 +-- src/http_jsc/websocket_client.rs | 64 +- .../WebSocketUpgradeClient.rs | 535 ++++++------ src/ini/lib.rs | 32 +- src/install/PackageInstaller.rs | 25 +- src/install/PackageManager.rs | 107 +-- .../PackageManagerDirectories.rs | 6 +- .../PackageManager/PackageManagerEnqueue.rs | 53 +- .../PackageManager/PopulateManifestCache.rs | 48 +- src/install/PackageManager/ProgressStrings.rs | 61 +- src/install/PackageManager/runTasks.rs | 35 +- .../updatePackageJSONAndInstall.rs | 5 +- src/install/auto_installer.rs | 17 +- src/install/hoisted_install.rs | 22 +- src/install/isolated_install.rs | 52 +- src/install/isolated_install/Installer.rs | 114 +-- src/install/lifecycle_script_runner.rs | 42 +- src/install/patch_install.rs | 21 +- src/install/resolvers/folder_resolver.rs | 45 +- src/install_jsc/ini_jsc.rs | 62 +- src/io/ParentDeathWatchdog.rs | 14 +- src/io/PipeReader.rs | 173 ++-- src/io/lib.rs | 70 +- src/io/posix_event_loop.rs | 14 +- src/io/source.rs | 17 +- src/io/windows_event_loop.rs | 27 +- src/js_parser/p.rs | 38 +- src/js_parser/scan/scan_imports.rs | 14 +- src/js_parser/visit/mod.rs | 4 +- src/js_parser_jsc/Macro.rs | 28 +- src/js_printer/renamer.rs | 41 +- src/jsc/ConcurrentPromiseTask.rs | 51 +- src/jsc/Debugger.rs | 114 +-- src/jsc/FetchHeaders.rs | 474 ++++++----- src/jsc/JSMap.rs | 12 +- src/jsc/JSPromise.rs | 30 +- src/jsc/MarkedArgumentBuffer.rs | 26 +- src/jsc/PosixSignalHandle.rs | 5 +- src/jsc/RuntimeTranspilerStore.rs | 104 +-- src/jsc/VirtualMachine.rs | 61 +- src/jsc/WorkTask.rs | 22 +- src/jsc/any_task_job.rs | 52 +- src/jsc/btjs.rs | 5 +- src/jsc/event_loop.rs | 25 +- src/jsc/hot_reloader.rs | 13 +- src/jsc/ipc.rs | 159 ++-- src/jsc/lib.rs | 6 +- src/jsc/rare_data.rs | 9 +- src/jsc/webcore_types.rs | 18 +- src/libuv_sys/libuv.rs | 43 +- src/opaque/lib.rs | 103 +++ src/parsers/toml.rs | 12 +- src/ptr/weak_ptr.rs | 64 +- src/resolver/fs.rs | 285 +++---- src/resolver/lib.rs | 149 ++-- src/resolver/package_json.rs | 18 +- src/resolver/resolver.rs | 194 +++-- src/router/lib.rs | 20 +- src/runtime/allocators/LinuxMemFdAllocator.rs | 14 +- src/runtime/api/Archive.rs | 32 +- src/runtime/api/BunObject.rs | 23 +- src/runtime/api/JSBundler.rs | 19 +- src/runtime/api/JSTranspiler.rs | 30 +- src/runtime/api/YAMLObject.rs | 4 +- src/runtime/api/bun/SSLContextCache.rs | 35 +- src/runtime/api/bun/SecureContext.rs | 4 +- src/runtime/api/bun/h2_frame_parser.rs | 803 +++++++++--------- src/runtime/api/bun/js_bun_spawn_bindings.rs | 62 +- src/runtime/api/bun/subprocess.rs | 36 +- src/runtime/api/bun/subprocess/Writable.rs | 62 +- src/runtime/api/cron.rs | 741 ++++++++-------- src/runtime/api/filesystem_router.rs | 46 +- src/runtime/api/html_rewriter.rs | 149 ++-- src/runtime/api/js_bundle_completion_task.rs | 52 +- src/runtime/api/output_file_jsc.rs | 4 +- src/runtime/api/standalone_graph_jsc.rs | 11 +- src/runtime/bake/DevServer.rs | 127 ++- src/runtime/bake/FrameworkRouter.rs | 16 +- src/runtime/bake/dev_server/mod.rs | 61 +- .../bake/dev_server/source_map_store.rs | 55 +- src/runtime/bake/production.rs | 83 +- src/runtime/cli/build_command.rs | 17 +- src/runtime/cli/bunx_command.rs | 18 +- src/runtime/cli/create_command.rs | 66 +- src/runtime/cli/exec_command.rs | 8 +- src/runtime/cli/filter_run.rs | 4 +- src/runtime/cli/init_command.rs | 8 +- src/runtime/cli/mod.rs | 21 +- src/runtime/cli/multi_run.rs | 4 +- src/runtime/cli/open.rs | 18 +- src/runtime/cli/outdated_command.rs | 18 +- src/runtime/cli/pack_command.rs | 28 +- src/runtime/cli/package_manager_command.rs | 13 +- src/runtime/cli/pm_trusted_command.rs | 17 +- src/runtime/cli/pm_update_package_json.rs | 28 +- src/runtime/cli/pm_version_command.rs | 9 +- src/runtime/cli/publish_command.rs | 44 +- src/runtime/cli/run_command.rs | 278 +++--- src/runtime/cli/scan_command.rs | 43 +- src/runtime/cli/test/Scanner.rs | 20 +- src/runtime/cli/test/parallel/Channel.rs | 35 +- src/runtime/cli/test_command.rs | 32 +- src/runtime/cli/update_interactive_command.rs | 28 +- src/runtime/cli/upgrade_command.rs | 4 +- src/runtime/crypto/PBKDF2.rs | 4 +- src/runtime/crypto/PasswordObject.rs | 15 +- src/runtime/dispatch.rs | 99 ++- src/runtime/dns_jsc/dns.rs | 85 +- src/runtime/ffi/ffi_body.rs | 177 ++-- src/runtime/hw_exports.rs | 9 +- src/runtime/image/Image.rs | 4 +- src/runtime/ipc_host.rs | 7 +- src/runtime/jsc_hooks.rs | 102 +-- src/runtime/napi/napi_body.rs | 15 +- src/runtime/node/node_cluster_binding.rs | 80 +- src/runtime/node/node_crypto_binding.rs | 6 +- src/runtime/node/node_fs.rs | 149 ++-- src/runtime/node/node_fs_watcher.rs | 50 +- src/runtime/node/types.rs | 29 +- src/runtime/node/win_watcher.rs | 261 +++--- src/runtime/node/zlib/NativeZlib.rs | 22 +- src/runtime/server/AnyRequestContext.rs | 12 +- src/runtime/server/FileRoute.rs | 6 +- src/runtime/server/HTMLBundle.rs | 42 +- src/runtime/server/NodeHTTPResponse.rs | 4 - src/runtime/server/RequestContext.rs | 155 +--- src/runtime/server/ServerConfig.rs | 8 +- src/runtime/server/StaticRoute.rs | 12 +- src/runtime/server/mod.rs | 204 ++--- src/runtime/server/server_body.rs | 285 +++---- src/runtime/shell/Builtin.rs | 34 +- src/runtime/shell/IOReader.rs | 179 ++-- src/runtime/shell/IOWriter.rs | 173 ++-- src/runtime/shell/builtin/cp.rs | 84 +- src/runtime/shell/dispatch_tasks.rs | 60 +- src/runtime/shell/interpreter.rs | 161 ++-- src/runtime/shell/shell_body.rs | 18 +- src/runtime/shell/states/Cmd.rs | 80 +- src/runtime/shell/states/CondExpr.rs | 16 +- src/runtime/shell/states/Expansion.rs | 45 +- src/runtime/shell/subproc.rs | 54 +- src/runtime/socket/Listener.rs | 167 ++-- src/runtime/socket/WindowsNamedPipeContext.rs | 19 +- src/runtime/socket/socket_body.rs | 57 +- src/runtime/socket/udp_socket.rs | 16 +- src/runtime/test_runner/Collection.rs | 9 +- src/runtime/test_runner/Execution.rs | 210 +++-- src/runtime/test_runner/Order.rs | 26 +- src/runtime/test_runner/ScopeFunctions.rs | 14 +- src/runtime/test_runner/bun_test.rs | 137 ++- src/runtime/test_runner/debug.rs | 10 +- src/runtime/test_runner/expect.rs | 22 +- src/runtime/test_runner/pretty_format.rs | 19 +- src/runtime/test_runner/snapshot.rs | 29 +- src/runtime/timer/EventLoopDelayMonitor.rs | 9 +- src/runtime/timer/mod.rs | 26 +- src/runtime/valkey_jsc/js_valkey.rs | 11 +- src/runtime/webcore/ArrayBufferSink.rs | 18 +- src/runtime/webcore/BakeResponse.rs | 37 +- src/runtime/webcore/Blob.rs | 44 +- src/runtime/webcore/Body.rs | 91 +- src/runtime/webcore/ByteBlobLoader.rs | 130 +-- src/runtime/webcore/FileReader.rs | 43 +- src/runtime/webcore/FileSink.rs | 198 +++-- src/runtime/webcore/ObjectURLRegistry.rs | 3 +- src/runtime/webcore/ReadableStream.rs | 41 +- src/runtime/webcore/Request.rs | 160 ++-- src/runtime/webcore/Response.rs | 222 +---- src/runtime/webcore/S3Client.rs | 35 +- src/runtime/webcore/S3File.rs | 98 +-- src/runtime/webcore/Sink.rs | 118 ++- src/runtime/webcore/blob/Store.rs | 8 +- src/runtime/webcore/blob/copy_file.rs | 105 ++- src/runtime/webcore/blob/read_file.rs | 45 +- src/runtime/webcore/blob/write_file.rs | 42 +- src/runtime/webcore/fetch.rs | 94 +- src/runtime/webcore/fetch/FetchTasklet.rs | 162 ++-- src/runtime/webcore/prompt.rs | 34 +- src/runtime/webcore/s3/client.rs | 20 +- src/runtime/webcore/s3/simple_request.rs | 16 +- src/runtime/webcore/streams.rs | 40 +- src/runtime/webcore/wasm_streaming.rs | 4 +- src/sourcemap_jsc/CodeCoverage.rs | 82 +- src/spawn/process.rs | 58 +- src/spawn/static_pipe_writer.rs | 16 +- src/sql_jsc/jsc.rs | 37 +- src/sql_jsc/mysql/JSMySQLConnection.rs | 34 +- src/sql_jsc/mysql/JSMySQLQuery.rs | 32 +- src/sql_jsc/mysql/MySQLConnection.rs | 93 +- src/sql_jsc/mysql/MySQLQuery.rs | 21 +- src/sql_jsc/mysql/MySQLStatement.rs | 14 +- src/sql_jsc/postgres/PostgresSQLConnection.rs | 72 +- src/sql_jsc/postgres/PostgresSQLQuery.rs | 85 +- src/sys/lib.rs | 89 +- src/threading/ThreadPool.rs | 27 +- src/uws_sys/Cargo.toml | 1 + src/uws_sys/ListenSocket.rs | 19 +- src/uws_sys/Loop.rs | 21 +- src/uws_sys/WebSocket.rs | 40 +- src/uws_sys/quic/Stream.rs | 11 +- src/watcher/Watcher.rs | 37 +- src/watcher/lib.rs | 5 +- test/js/bun/http/bun-server.test.ts | 19 + 238 files changed, 6980 insertions(+), 7430 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4f88b2de6b33..6fa3964e716e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -438,6 +438,7 @@ dependencies = [ "bun_options_types", "bun_parsers", "bun_paths", + "bun_ptr", "bun_resolver", "bun_standalone_graph", "bun_sys", @@ -2175,6 +2176,7 @@ dependencies = [ "bun_libuv_sys", "bun_opaque", "bun_paths", + "bun_ptr", "bun_windows_sys", "const_format", "enum-map", diff --git a/src/ast/e.rs b/src/ast/e.rs index 8bb8385a23ce..0c9782485cca 100644 --- a/src/ast/e.rs +++ b/src/ast/e.rs @@ -2662,9 +2662,8 @@ mod json_tape_tests { self.0 } /// `Parser::tape_mut` — a fresh reborrow of the root pointer per call. - #[allow(clippy::mut_from_ref)] - fn get(&self) -> &mut JsonTape { - // SAFETY: sole owner; each call hands out one short-lived borrow. + fn get(&mut self) -> &mut JsonTape { + // SAFETY: sole owner; `&mut self` makes the reborrow exclusive. unsafe { &mut *self.0.as_ptr() } } } @@ -2682,7 +2681,7 @@ mod json_tape_tests { /// the inner node must survive, because `properties()` is read afterwards. #[test] fn object_json_survives_later_tape_writes() { - let tape = TapeOwner::new(); + let mut tape = TapeOwner::new(); // Inner `{"b": null}`. let kb = tape.get().alloc_str(b"b"); @@ -2707,7 +2706,7 @@ mod json_tape_tests { #[test] fn array_json_survives_later_tape_writes() { - let tape = TapeOwner::new(); + let mut tape = TapeOwner::new(); let (first, count) = tape.get().append_items(&[JsonValue::Null], &[]); // SAFETY: the tape's own pointer, as `Parser` passes it. @@ -2741,7 +2740,7 @@ mod json_tape_tests { /// later strings spill into new chunks. #[test] fn alloc_str_chunks_never_move() { - let tape = TapeOwner::new(); + let mut tape = TapeOwner::new(); let a = tape.get().alloc_str(b"first"); // Force a fresh chunk: bigger than what is left in the current one. let big = vec![b'x'; JsonTape::STR_CHUNK + 1]; diff --git a/src/bun_alloc/BufferFallbackAllocator.rs b/src/bun_alloc/BufferFallbackAllocator.rs index fd8af3529127..f64cf4e566e4 100644 --- a/src/bun_alloc/BufferFallbackAllocator.rs +++ b/src/bun_alloc/BufferFallbackAllocator.rs @@ -19,14 +19,14 @@ impl<'a> BufferFallbackAllocator<'a> { } } - pub fn allocator(&mut self) -> StdAllocator { + pub fn allocator(&self) -> StdAllocator { StdAllocator { - ptr: std::ptr::from_mut::(self).cast::(), + ptr: std::ptr::from_ref::(self).cast_mut().cast::(), vtable: &VTABLE, } } - pub fn reset(&mut self) { + pub fn reset(&self) { self.fixed.reset(); } } @@ -39,10 +39,11 @@ static VTABLE: AllocatorVTable = AllocatorVTable { }; unsafe fn alloc(ctx: *mut c_void, len: usize, alignment: Alignment, ra: usize) -> *mut u8 { - // SAFETY: ctx was set to `&mut BufferFallbackAllocator` in `allocator()`. - let self_: &mut BufferFallbackAllocator = - unsafe { &mut *ctx.cast::() }; - FixedBufferAllocator::alloc(&mut self_.fixed, len, alignment, ra) + // SAFETY: ctx was set to `&BufferFallbackAllocator` in `allocator()`. + let self_: &BufferFallbackAllocator = unsafe { &*ctx.cast::() }; + self_ + .fixed + .alloc(len, alignment, ra) .or_else(|| self_.fallback.raw_alloc(len, alignment, ra)) .unwrap_or(core::ptr::null_mut()) } @@ -54,11 +55,10 @@ unsafe fn resize( new_len: usize, ra: usize, ) -> bool { - // SAFETY: ctx was set to `&mut BufferFallbackAllocator` in `allocator()`. - let self_: &mut BufferFallbackAllocator = - unsafe { &mut *ctx.cast::() }; + // SAFETY: ctx was set to `&BufferFallbackAllocator` in `allocator()`. + let self_: &BufferFallbackAllocator = unsafe { &*ctx.cast::() }; if self_.fixed.owns_ptr(buf.as_ptr()) { - return FixedBufferAllocator::resize(&mut self_.fixed, buf, alignment, new_len, ra); + return self_.fixed.resize(buf, alignment, new_len, ra); } self_.fallback.raw_resize(buf, alignment, new_len, ra) } @@ -70,11 +70,12 @@ unsafe fn remap( new_len: usize, ra: usize, ) -> *mut u8 { - // SAFETY: ctx was set to `&mut BufferFallbackAllocator` in `allocator()`. - let self_: &mut BufferFallbackAllocator = - unsafe { &mut *ctx.cast::() }; + // SAFETY: ctx was set to `&BufferFallbackAllocator` in `allocator()`. + let self_: &BufferFallbackAllocator = unsafe { &*ctx.cast::() }; if self_.fixed.owns_ptr(memory.as_ptr()) { - return FixedBufferAllocator::remap(&mut self_.fixed, memory, alignment, new_len, ra) + return self_ + .fixed + .remap(memory, alignment, new_len, ra) .unwrap_or(core::ptr::null_mut()); } self_ @@ -84,11 +85,10 @@ unsafe fn remap( } unsafe fn free(ctx: *mut c_void, buf: &mut [u8], alignment: Alignment, ra: usize) { - // SAFETY: ctx was set to `&mut BufferFallbackAllocator` in `allocator()`. - let self_: &mut BufferFallbackAllocator = - unsafe { &mut *ctx.cast::() }; + // SAFETY: ctx was set to `&BufferFallbackAllocator` in `allocator()`. + let self_: &BufferFallbackAllocator = unsafe { &*ctx.cast::() }; if self_.fixed.owns_ptr(buf.as_ptr()) { - return FixedBufferAllocator::free(&mut self_.fixed, buf, alignment, ra); + return self_.fixed.free(buf, alignment, ra); } self_.fallback.raw_free(buf, alignment, ra) } diff --git a/src/bun_alloc/lib.rs b/src/bun_alloc/lib.rs index 5f95396c96d1..61ae2ef21f33 100644 --- a/src/bun_alloc/lib.rs +++ b/src/bun_alloc/lib.rs @@ -10,6 +10,7 @@ // Used for the per-allocation hot-path TLS in `ast_alloc::AST_ALLOC`. #![feature(thread_local)] +use core::cell::{Cell, UnsafeCell}; use core::fmt::Write as _; use core::mem::{MaybeUninit, size_of}; use core::ptr::{NonNull, addr_of_mut}; @@ -183,51 +184,65 @@ impl StdAllocator { /// Bump allocator over a caller-owned buffer. pub struct FixedBufferAllocator<'a> { - end: usize, - buffer: &'a mut [u8], + /// `Cell` + `UnsafeCell` so every method takes `&self`: the vtable thunks in + /// `BufferFallbackAllocator` only ever get a shared ref out of their `ctx`, + /// and `alloc` hands out `*mut u8` into `buffer`. + end: Cell, + buffer: &'a UnsafeCell<[u8]>, } impl<'a> FixedBufferAllocator<'a> { #[inline] pub fn init(buffer: &'a mut [u8]) -> Self { - Self { end: 0, buffer } + Self { + end: Cell::new(0), + buffer: UnsafeCell::from_mut(buffer), + } } #[inline] - pub fn reset(&mut self) { - self.end = 0; + fn base(&self) -> *mut u8 { + self.buffer.get().cast::() + } + #[inline] + fn capacity(&self) -> usize { + self.buffer.get().len() + } + #[inline] + pub fn reset(&self) { + self.end.set(0); } #[inline] pub fn owns_ptr(&self, p: *const u8) -> bool { - let base = self.buffer.as_ptr() as usize; - let q = p as usize; - q >= base && q < base + self.buffer.len() - } - pub fn alloc(&mut self, len: usize, alignment: Alignment, _ra: usize) -> Option<*mut u8> { - let base = self.buffer.as_mut_ptr() as usize; - let aligned = - (base + self.end + alignment.to_byte_units() - 1) & !(alignment.to_byte_units() - 1); - let new_end = (aligned - base).checked_add(len)?; - if new_end > self.buffer.len() { + let base = self.base().addr(); + let q = p.addr(); + q >= base && q < base + self.capacity() + } + pub fn alloc(&self, len: usize, alignment: Alignment, _ra: usize) -> Option<*mut u8> { + let base = self.base(); + let aligned = (base.addr() + self.end.get() + alignment.to_byte_units() - 1) + & !(alignment.to_byte_units() - 1); + let new_end = (aligned - base.addr()).checked_add(len)?; + if new_end > self.capacity() { return None; } - self.end = new_end; - Some(aligned as *mut u8) + self.end.set(new_end); + Some(base.with_addr(aligned)) } - pub fn resize(&mut self, buf: &mut [u8], _a: Alignment, new_len: usize, _ra: usize) -> bool { + pub fn resize(&self, buf: &mut [u8], _a: Alignment, new_len: usize, _ra: usize) -> bool { // Only the last allocation can grow; shrinks always succeed. - let buf_end = buf.as_ptr() as usize - self.buffer.as_ptr() as usize + buf.len(); - if buf_end != self.end { + let buf_end = buf.as_ptr().addr() - self.base().addr() + buf.len(); + if buf_end != self.end.get() { return new_len <= buf.len(); } let new_end = buf_end - buf.len() + new_len; - if new_end > self.buffer.len() { + if new_end > self.capacity() { return false; } - self.end = new_end; + self.end.set(new_end); true } #[inline] pub fn remap( - &mut self, + &self, buf: &mut [u8], a: Alignment, new_len: usize, @@ -240,11 +255,11 @@ impl<'a> FixedBufferAllocator<'a> { } } #[inline] - pub fn free(&mut self, buf: &mut [u8], _a: Alignment, _ra: usize) { + pub fn free(&self, buf: &mut [u8], _a: Alignment, _ra: usize) { // Only the last allocation can be freed. - let buf_end = buf.as_ptr() as usize - self.buffer.as_ptr() as usize + buf.len(); - if buf_end == self.end { - self.end -= buf.len(); + let buf_end = buf.as_ptr().addr() - self.base().addr() + buf.len(); + if buf_end == self.end.get() { + self.end.set(self.end.get() - buf.len()); } } } diff --git a/src/bundler/BundleThread.rs b/src/bundler/BundleThread.rs index fb6644058b52..64efffe86ed8 100644 --- a/src/bundler/BundleThread.rs +++ b/src/bundler/BundleThread.rs @@ -274,8 +274,7 @@ impl BundleThread { // `completion` can be borrowed again below. let transpiler_ptr: *mut Transpiler<'_> = transpiler; let run = completion.init_and_run( - // SAFETY: `transpiler` lives in `bump` for the duration of `heap`. - unsafe { &mut *transpiler_ptr }, + transpiler, bump, // `WorkPool::get()` returns `&'static ThreadPool`; pass as raw so // the impl can hand it to `BundleV2::init` (which stores `*mut`). diff --git a/src/bundler/LinkerContext.rs b/src/bundler/LinkerContext.rs index 8ce69747981a..4a1744d61e36 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -151,7 +151,10 @@ pub use crate::DeferredBatchTask::DeferredBatchTask; pub use crate::ParseTask; pub struct LinkerContext<'a> { - pub parse_graph: *mut Graph<'a>, + /// Backref into `BundleV2.graph`, a sibling field of `BundleV2.linker` + /// (= `*self`), assigned in [`Self::load`]. `Option` because `Default` + /// precedes `load()`. `Copy`, so split-borrow sites read it out by value. + pub parse_graph: Option>>, pub graph: LinkerGraph<'a>, /// Backref into `Transpiler.log`, assigned in [`Self::load`]. Stored as a /// raw pointer (like `parse_graph` / `resolver`) so `Default` can be @@ -217,7 +220,7 @@ unsafe impl<'a> Sync for LinkerContext<'a> {} impl<'a> Default for LinkerContext<'a> { fn default() -> Self { Self { - parse_graph: core::ptr::null_mut(), + parse_graph: None, graph: Default::default(), log: core::ptr::null_mut(), resolver: None, @@ -267,13 +270,10 @@ impl<'a> LinkerContext<'a> { /// `self.parse_graph` field directly. #[inline] pub fn parse_graph(&self) -> &Graph<'_> { - debug_assert!( - !self.parse_graph.is_null(), - "LinkerContext.parse_graph accessed before load()" - ); - // SAFETY: non-null backref into `BundleV2.graph`, valid for the link - // step, disjoint from `*self` (= `BundleV2.linker`). - unsafe { &*self.parse_graph } + self.parse_graph + .as_ref() + .expect("LinkerContext.parse_graph accessed before load()") + .get() } /// Exclusive accessor for the parse-side graph. See [`Self::parse_graph`] @@ -282,13 +282,13 @@ impl<'a> LinkerContext<'a> { /// borrows. #[inline] pub fn parse_graph_mut(&mut self) -> &mut Graph<'a> { - debug_assert!( - !self.parse_graph.is_null(), - "LinkerContext.parse_graph accessed before load()" - ); - // SAFETY: non-null backref into `BundleV2.graph`, disjoint from - // `*self`; `&mut self` excludes other safe borrows of the linker. - unsafe { &mut *self.parse_graph } + let parse_graph = self + .parse_graph + .expect("LinkerContext.parse_graph accessed before load()"); + // SAFETY: backref into `BundleV2.graph`, disjoint from `*self`; `&mut + // self` excludes other safe borrows of the linker. `as_ptr` (not + // `get_mut`) so the borrow is tied to `&mut self`, not to the local. + unsafe { &mut *parse_graph.as_ptr() } } /// Shared-read accessor for the resolver. @@ -304,25 +304,19 @@ impl<'a> LinkerContext<'a> { .get() } - /// Mutable projection of the `r#loop` BACKREF for `AnyEventLoop` dispatch - /// (`enqueue_task_concurrent*`, `tick`). Centralises the raw `NonNull` - /// deref so the three callers (`BundleV2::any_loop_mut`, `ParseTask` / + /// Shared projection of the `r#loop` BACKREF for `AnyEventLoop` dispatch + /// (`enqueue_task_concurrent*`). Centralises the raw `NonNull` deref so the + /// three callers (`BundleV2::any_loop`, `ParseTask` / /// `ServerComponentParseTask` completion) are safe. /// - /// `&self` receiver (not `&mut self`): the loop storage is **disjoint** - /// from `LinkerContext` (it lives in the `BundleThread` / runtime arena — - /// see [`EventLoop`]), and worker-thread completions reach this through a - /// `BackRef` (`&` only). + /// `&` not `&mut`: dispatch needs mutation, not exclusivity — both variants + /// enqueue through an MPSC queue whose `push` is `&self`. Worker-thread + /// completions reach this concurrently through a `BackRef`. #[inline] - #[allow(clippy::mut_from_ref)] - pub fn any_loop_mut(&self) -> Option<&mut bun_event_loop::AnyEventLoop<'static>> { + pub fn any_loop(&self) -> Option<&bun_event_loop::AnyEventLoop<'static>> { // SAFETY: BACKREF — set once in `BundleV2::init` from a loop that // outlives the bundle pass; the pointee is disjoint from `*self`. - // Exclusivity: `Js { owner }.enqueue_task_concurrent` is `&self` - // (MPSC), and `Mini.enqueue_task_concurrent_with_extra_ctx` only - // pushes to an MPSC queue + writes the caller-owned intrusive task - // node, so concurrent worker completions do not alias loop state. - self.r#loop.map(|p| unsafe { &mut *p.as_ptr() }) + self.r#loop.map(|p| unsafe { &*p.as_ptr() }) } /// Shared-read accessor for the bundler log. @@ -491,8 +485,9 @@ impl<'a> LinkerContext<'a> { ) -> Result<(), BunError> { let _trace = bun::perf::trace("Bundler.CloneLinkerGraph"); // SAFETY: field-disjoint with `self` (= `(*bundle).linker`); `parse_graph` - // is a `*mut Graph` backref so no `&mut` is materialized. - self.parse_graph = unsafe { core::ptr::addr_of_mut!((*bundle).graph) }; + // is a backref so no `&mut` is materialized. + self.parse_graph = + Some(unsafe { bun_ptr::BackRef::from_raw(core::ptr::addr_of_mut!((*bundle).graph)) }); // SAFETY: field-disjoint scalar read; `transpiler` is itself a `*mut`. let dyn_entry_points = unsafe { &mut *core::ptr::addr_of_mut!((*bundle).dynamic_import_entry_points) }; @@ -521,16 +516,19 @@ impl<'a> LinkerContext<'a> { // caller-owned slice into the linker arena. self.graph.reachable_files = reachable.to_vec(); - // SAFETY: parse_graph is valid backref just assigned above - let sources: &[Source] = unsafe { (*self.parse_graph).input_files.items_source() }; + // Backref copy (assigned just above): the borrows below are tied to this + // local, leaving `&mut self.graph` disjoint. + let parse_graph = self + .parse_graph + .expect("LinkerContext.parse_graph accessed before load()"); + let sources: &[Source] = parse_graph.input_files.items_source(); self.graph.load( entry_points, sources, server_component_boundaries, dyn_entry_points.keys(), - // SAFETY: parse_graph backref - unsafe { &(*self.parse_graph).entry_point_original_names }, + &parse_graph.entry_point_original_names, )?; dyn_entry_points.clear_retaining_capacity(); @@ -670,7 +668,10 @@ impl<'a> LinkerContext<'a> { // Note: go through raw pointers and reborrow per use to avoid holding // overlapping `&`/`&mut` into `parse_graph.html_imports` and // `parse_graph.input_files`. - let parse_graph: *mut Graph<'a> = self.parse_graph; + let parse_graph: *mut Graph<'a> = self + .parse_graph + .expect("LinkerContext.parse_graph accessed before load()") + .as_ptr(); // SAFETY: see above; sole accessor of `html_imports` for this scope. let server_len = unsafe { (*parse_graph).html_imports.server_source_indices.len() }; if server_len > 0 { @@ -775,7 +776,10 @@ impl<'a> LinkerContext<'a> { // reallocate inside `validate_tla`; we cache raw column pointers // and reborrow per call to satisfy borrowck (`&mut self` is held // across the recursion). - let parse_graph: *mut Graph<'a> = self.parse_graph; + let parse_graph: *mut Graph<'a> = self + .parse_graph + .expect("LinkerContext.parse_graph accessed before load()") + .as_ptr(); let import_records_list: *const [bun_ast::import_record::List<'a>] = self.graph.ast.items_import_records(); let flags: *mut [crate::js_meta::Flags] = self.graph.meta.items_flags_mut(); @@ -1671,18 +1675,23 @@ impl<'a> GenerateChunkCtx<'a> { unsafe { &*LinkerContext::bundle_v2_ptr(self.c.as_mut_ptr()) } } - /// Mutable view of the owning `LinkerContext`. Centralizes the `unsafe` - /// deref of the `c: *mut LinkerContext` backref (set in - /// `generate_chunks_in_parallel`); callers previously open-coded - /// `unsafe { &mut *ctx.c }`. The per-chunk tasks each touch a disjoint - /// chunk, so the linker fields they write don't alias across tasks. + /// Exclusive view of the owning `LinkerContext` behind the `c` backref. + /// + /// # Safety + /// `GenerateChunkCtx` is `Copy + Sync` (required by `each_ptr`) and one + /// copy is handed to every `generate_chunk` worker task, so exclusivity + /// cannot be enforced by the type system here. The caller must guarantee + /// that no other borrow of the `LinkerContext` overlaps the returned + /// `&mut` — including one minted by a peer task's `c()`, or a `&BundleV2` + /// from [`bundle`](Self::bundle), which aliases the same memory — and that + /// every linker field it touches is disjoint from the fields peer tasks + /// touch. Note `generate_isolated_hash` writes `input_files` rows indexed + /// by source, not by chunk. #[inline] #[allow(clippy::mut_from_ref)] - pub fn c(&self) -> &mut LinkerContext<'a> { - // SAFETY: ParentRef into `BundleV2.linker`, valid for the - // chunk-generation pass; this task's chunk row is disjoint from peers'. - // Constructed via `from_raw_mut` (write provenance) in - // `generate_chunks_in_parallel`. + pub unsafe fn c(&self) -> &mut LinkerContext<'a> { + // SAFETY: caller contract above, plus `from_raw_mut` write provenance + // from `generate_chunks_in_parallel`; parent is live for the link step. unsafe { self.c.assume_mut() } } } @@ -1770,8 +1779,12 @@ impl<'a> LinkerContext<'a> { // that live in separate parts in the same file must not be merged. This only // needs to be done for JavaScript files, not CSS files. if let crate::chunk::Content::Javascript(js) = &chunk.content { - // SAFETY: parse_graph backref; exclusive access via &mut *. - let sources = unsafe { (*self.parse_graph).input_files.items_source_mut() }; + let parse_graph: *mut Graph<'a> = self + .parse_graph + .expect("LinkerContext.parse_graph accessed before load()") + .as_ptr(); + // SAFETY: parse_graph backref; exclusive access via the raw ptr. + let sources = unsafe { (*parse_graph).input_files.items_source_mut() }; for part_range in js.parts_in_chunk_in_order.iter() { let source: &mut Source = &mut sources[part_range.source_index.get() as usize]; @@ -2187,9 +2200,12 @@ impl<'a> LinkerContext<'a> { ..Default::default() }]; - // SAFETY: parse_graph backref; raw deref because `parse_graph` is held - // across `RequireOrImportMetaCallback::init(self)` (`&mut self`) below. - let parse_graph = unsafe { &*self.parse_graph }; + // Backref copy: the `&Graph` is tied to this local, not to `*self`, which + // `RequireOrImportMetaCallback::init(self)` below borrows mutably. + let parse_graph_ref = self + .parse_graph + .expect("LinkerContext.parse_graph accessed before load()"); + let parse_graph = parse_graph_ref.get(); // Note: reshaped for borrowck — `Options` borrows `ts_enums` / // `line_offset_tables` / `mangled_props` from `self.graph`, but the @@ -2355,9 +2371,12 @@ impl<'a> LinkerContext<'a> { let all_css_asts = self.graph.ast.items_css(); let all_symbols: &[bun_ast::symbol::List<'a>] = self.graph.ast.items_symbols(); - // SAFETY: parse_graph backref; raw deref because `all_sources` is held - // across `&mut self.mangled_props` below (split borrow). - let all_sources: &[Source] = unsafe { (*self.parse_graph).input_files.items_source() }; + // Backref copy: `all_sources` is tied to this local, not to `*self`, so + // `&mut self.mangled_props` below stays disjoint (split borrow). + let parse_graph_ref = self + .parse_graph + .expect("LinkerContext.parse_graph accessed before load()"); + let all_sources: &[Source] = parse_graph_ref.input_files.items_source(); // Collect all local css names let mut local_css_names: HashMap = HashMap::new(); @@ -3006,11 +3025,14 @@ impl<'a> LinkerContext<'a> { Ok(i) => i, Err(_) => unreachable!(), }; - // SAFETY: parse_graph backref into BundleV2.graph; the input_files SoA - // is monotonically grown and never freed for the link step's lifetime, - // so the element address is stable. `'static` is a white lie matching - // the `*mut Graph` erasure on `self.parse_graph`. - unsafe { &*core::ptr::from_ref(&(*self.parse_graph).input_files.items_source()[index]) } + let parse_graph = self + .parse_graph + .expect("LinkerContext.parse_graph accessed before load()"); + // SAFETY: parse_graph backrefs into BundleV2.graph; the input_files SoA is + // monotonically grown and never freed for the link step's lifetime, so the + // element address is stable. The `'static` is a white lie matching the + // `*mut Graph` erasure on `self.parse_graph`. + unsafe { &*core::ptr::from_ref(&parse_graph.input_files.items_source()[index]) } } /// `log` is an explicit parameter (not `self.log`) because the dev-server diff --git a/src/bundler/ParseTask.rs b/src/bundler/ParseTask.rs index 1329ab2bbbdb..63a6cb35cf99 100644 --- a/src/bundler/ParseTask.rs +++ b/src/bundler/ParseTask.rs @@ -2796,7 +2796,7 @@ pub mod parse_worker { drop(core::mem::take(&mut this.jsx)); // `worker.ctx` is a `BackRef` (safe `Deref`); the BACKREF deref - // of `linker.r#loop` is centralised in `LinkerContext::any_loop_mut`. + // of `linker.r#loop` is centralised in `LinkerContext::any_loop`. // // The loop is effectively non-optional — `BundleV2::init` // always sets `linker.r#loop` before scheduling any ParseTask. Running @@ -2806,7 +2806,7 @@ pub mod parse_worker { match worker .ctx .linker - .any_loop_mut() + .any_loop() .expect("BundleV2.linker.loop must be set before scheduling ParseTask") { bun_event_loop::AnyEventLoop::Js { owner } => { diff --git a/src/bundler/ServerComponentParseTask.rs b/src/bundler/ServerComponentParseTask.rs index ea5a444aed3a..fce6a81212f6 100644 --- a/src/bundler/ServerComponentParseTask.rs +++ b/src/bundler/ServerComponentParseTask.rs @@ -108,7 +108,7 @@ fn task_callback_wrap(thread_pool_task: *mut ThreadPoolTask) { let result = bun_core::heap::into_raw(result); // `worker.ctx` is a `BackRef` (safe `Deref`); the BACKREF deref - // of `linker.r#loop` is centralised in `LinkerContext::any_loop_mut`. + // of `linker.r#loop` is centralised in `LinkerContext::any_loop`. // // The loop is effectively non-optional — `BundleV2::init` // always sets `linker.r#loop` before scheduling any ServerComponentParseTask. @@ -118,7 +118,7 @@ fn task_callback_wrap(thread_pool_task: *mut ThreadPoolTask) { match worker .ctx .linker - .any_loop_mut() + .any_loop() .expect("BundleV2.linker.loop must be set before scheduling ServerComponentParseTask") { bun_event_loop::AnyEventLoop::Js { owner } => { diff --git a/src/bundler/ThreadPool.rs b/src/bundler/ThreadPool.rs index 13ec1639add6..6ad513fa4a30 100644 --- a/src/bundler/ThreadPool.rs +++ b/src/bundler/ThreadPool.rs @@ -593,10 +593,10 @@ impl Worker { // only ever invoked by the thread pool against a `Worker` enqueued via // `deinit_soon`, so provenance covers the full `Worker` allocation. let this: *mut Worker = unsafe { bun_core::from_field_ptr!(Worker, deinit_task, task) }; - // SAFETY: `deinit_soon` schedules this exactly once on a live - // heap-allocated `Worker`; the idle-task fires on the worker's own OS - // thread with no other live borrow, so we hold exclusive ownership. - unsafe { Self::deinit(this) }; + // SAFETY: `deinit_soon` schedules this exactly once on a live `Worker` + // heap-allocated by `get_worker`; the idle-task fires on the worker's + // own OS thread with no other live borrow, so the Box is exclusive. + unsafe { Self::deinit(bun_core::heap::take(this)) }; } pub fn deinit_soon(&mut self) { @@ -613,20 +613,16 @@ impl Worker { // `ast_memory_store` mi_heap (every `AstAlloc` buffer the inline // parse produced) and `data.transpiler` per `Bun.build()` call. // - // SAFETY: `self` is the heap-allocated Worker; sole owner now that - // the caller is about to `clear_retaining_capacity()` the - // `workers_assignments` map. - unsafe { Self::deinit(std::ptr::from_mut::(self)) }; + // SAFETY: `self` is the Worker heap-allocated by `get_worker`; sole + // owner now that the caller is about to `clear_retaining_capacity()` + // the `workers_assignments` map. + unsafe { Self::deinit(bun_core::heap::take(std::ptr::from_mut::(self))) }; } } /// Takes ownership of the heap allocation and frees it. - /// - /// # Safety - /// `this` must have come from `heap::alloc` in [`ThreadPool::get_worker`]. - pub unsafe fn deinit(this: *mut Worker) { - // SAFETY: caller contract. - let worker = unsafe { &mut *this }; + pub fn deinit(mut self: Box) { + let worker = &mut *self; if worker.has_created { // `wire_after_move` boxed a `bun_js_parser_jsc::Macro::MacroContext` // behind `macro_context.data` (raw `*mut`, no `Drop` glue); @@ -663,11 +659,9 @@ impl Worker { if worker.has_created { worker.heap = None; } - // SAFETY: caller contract — `this` was heap-allocated via `get_worker`. - // Runs full field drop glue: remaining `Option` fields are `None` - // (no-op), `ast_memory_store` is `ManuallyDrop` (no auto-drop), so no - // double-free; defends against future `Drop`-carrying fields. - unsafe { bun_core::heap::destroy(this) }; + // `self` drops here: full field drop glue (remaining `Option` fields are + // `None`, `ast_memory_store` is `ManuallyDrop`), then the allocation is + // freed — same as the `heap::destroy` this replaced. } // returns `&'static mut` (detached) — the `Worker` is diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 3c3c711cf403..cda2209a43f7 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -178,10 +178,10 @@ impl<'a> BundleV2<'a> { /// `switch (this.loop().*)` — `linker.loop` is a non-owning backref to the /// `AnyEventLoop` that owns this bundle pass and outlives it. #[inline] - pub fn any_loop_mut(&mut self) -> &mut bun_event_loop::AnyEventLoop<'static> { - // BACKREF deref centralised in `LinkerContext::any_loop_mut`. + pub fn any_loop(&self) -> &bun_event_loop::AnyEventLoop<'static> { + // BACKREF deref centralised in `LinkerContext::any_loop`. self.linker - .any_loop_mut() + .any_loop() .expect("BundleV2.linker.loop must be set before plugins run") } @@ -1112,7 +1112,7 @@ pub mod bv2_impl { /// are the real lower-tier `bun_event_loop` types, so `dispatch()` / /// `run_on_js_thread()` are implemented inherently (no T6 hook). pub struct Resolve { - pub bv2: *mut BundleV2<'static>, + pub bv2: Option>>, pub import_record: MiniImportRecord, pub value: ResolveValue, pub js_task: bun_event_loop::AnyTask::AnyTask, @@ -1122,7 +1122,7 @@ pub mod bv2_impl { impl Default for Resolve { fn default() -> Self { Self { - bv2: core::ptr::null_mut(), + bv2: None, import_record: MiniImportRecord::default(), value: ResolveValue::Pending, js_task: bun_event_loop::AnyTask::AnyTask::default(), @@ -1133,9 +1133,13 @@ pub mod bv2_impl { impl Resolve { pub fn init(bv2: &mut BundleV2<'_>, record: MiniImportRecord) -> Self { Self { - // SAFETY: lifetime erased — Resolve is owned by the dispatch - // chain and never outlives `bv2`. - bv2: std::ptr::from_mut::>(bv2).cast::>(), + // SAFETY: write provenance from `ptr::from_mut`; the bundle + // outlives the dispatch chain that owns this `Resolve`. + bv2: Some(unsafe { + bun_ptr::ParentRef::from_raw_mut( + std::ptr::from_mut::>(bv2).cast::>(), + ) + }), import_record: record, value: ResolveValue::Pending, js_task: bun_event_loop::AnyTask::AnyTask::default(), @@ -1152,29 +1156,29 @@ pub mod bv2_impl { }; let task = bun_event_loop::ConcurrentTask::ConcurrentTask::create(self.js_task.task()); - // SAFETY: `bv2` is a valid backref set by `init`; plugins is - // Some (asserted by `enqueue_on_js_loop_for_plugins`). - unsafe { (*self.bv2).enqueue_on_js_loop_for_plugins(task) }; + // SAFETY: backref set by `init`; no other borrow of the bundle is + // live, and enqueueing a task cannot re-enter JS. + unsafe { self.bv2.expect("bv2").assume_mut() } + .enqueue_on_js_loop_for_plugins(task); } pub fn run_on_js_thread(&mut self) { let kind = self.import_record.kind; // reshaped for borrowck — capture the erased self // pointer before borrowing fields immutably for the FFI call. let self_ptr = std::ptr::from_mut::(self).cast::(); - // SAFETY: `bv2` is a valid backref set by `init`; the plugin - // storage is disjoint from `self`, so the `&mut JSBundlerPlugin` - // returned by `plugins_mut()` does not alias the - // `&self.import_record.*` borrows below. - unsafe { &mut *self.bv2 } - .plugins_mut() - .expect("plugins") - .match_on_resolve( - &self.import_record.specifier, - &self.import_record.namespace, - &self.import_record.source_file, - self_ptr, - kind, - ); + // Copy the plugin backref out and drop the `&BundleV2`: the call + // below re-enters JS, which re-derives `&mut BundleV2` from `bv2`. + let bv2 = self.bv2.expect("bv2"); + let mut plugins = bv2.get().plugins.expect("plugins"); + // SAFETY: opaque C++ object owned by the completion task / + // DevServer; a distinct allocation from `self` and the bundle. + unsafe { plugins.as_mut() }.match_on_resolve( + &self.import_record.specifier, + &self.import_record.namespace, + &self.import_record.source_file, + self_ptr, + kind, + ); } fn run_on_js_thread_wrap( ctx: *mut core::ffi::c_void, @@ -1210,7 +1214,7 @@ pub mod bv2_impl { /// Task driving an onLoad plugin invocation for one source file. pub struct Load { - pub bv2: *mut BundleV2<'static>, + pub bv2: Option>>, pub source_index: bun_ast::Index, pub default_loader: Loader, pub path: Box<[u8]>, @@ -1234,7 +1238,13 @@ pub mod bv2_impl { .loader(&bv2.transpiler.options.loaders) .unwrap_or(Loader::Js); Self { - bv2: std::ptr::from_mut::>(bv2).cast::>(), + // SAFETY: write provenance from `ptr::from_mut`; the bundle + // outlives the dispatch chain that owns this `Load`. + bv2: Some(unsafe { + bun_ptr::ParentRef::from_raw_mut( + std::ptr::from_mut::>(bv2).cast::>(), + ) + }), parse_task: bun_ptr::BackRef::new_mut(parse), source_index: parse.source_index, default_loader, @@ -1257,7 +1267,7 @@ pub mod bv2_impl { /// must keep the raw deref + SAFETY note locally. #[inline] pub fn bv2_ptr(&self) -> *mut BundleV2<'static> { - self.bv2 + self.bv2.expect("bv2").as_mut_ptr() } /// Shared access to the heap-allocated `ParseTask` this load wraps. /// @@ -1294,11 +1304,10 @@ pub mod bv2_impl { }; let concurrent_task = bun_event_loop::ConcurrentTask::ConcurrentTask::create(self.js_task.task()); - // SAFETY: `bv2` is a valid backref; plugins is Some (asserted - // by `enqueue_on_js_loop_for_plugins`). - unsafe { - (*self.bv2).enqueue_on_js_loop_for_plugins(concurrent_task); - } + // SAFETY: backref set by `init`; no other borrow of the bundle is + // live, and enqueueing a task cannot re-enter JS. + unsafe { self.bv2.expect("bv2").assume_mut() } + .enqueue_on_js_loop_for_plugins(concurrent_task); } pub fn run_on_js_thread(&mut self) { let is_server_side = self.bake_graph() != crate::bake_types::Graph::Client; @@ -1306,20 +1315,19 @@ pub mod bv2_impl { // reshaped for borrowck — capture the erased self // pointer before borrowing fields immutably for the FFI call. let self_ptr = std::ptr::from_mut::(self).cast::(); - // SAFETY: `bv2` is a valid backref set by `init`; the plugin - // storage is disjoint from `self`, so the `&mut JSBundlerPlugin` - // returned by `plugins_mut()` does not alias the - // `&self.path` / `&self.namespace` borrows below. - unsafe { &mut *self.bv2 } - .plugins_mut() - .expect("plugins") - .match_on_load( - &self.path, - &self.namespace, - self_ptr, - default_loader, - is_server_side, - ); + // Copy the plugin backref out and drop the `&BundleV2`: the call + // below re-enters JS, which re-derives `&mut BundleV2` from `bv2`. + let bv2 = self.bv2.expect("bv2"); + let mut plugins = bv2.get().plugins.expect("plugins"); + // SAFETY: opaque C++ object owned by the completion task / + // DevServer; a distinct allocation from `self` and the bundle. + unsafe { plugins.as_mut() }.match_on_load( + &self.path, + &self.namespace, + self_ptr, + default_loader, + is_server_side, + ); } fn run_on_js_thread_wrap( ctx: *mut core::ffi::c_void, @@ -1563,8 +1571,8 @@ pub mod bv2_impl { } // From bake where the loop running the bundle is also the loop running // the plugins. - // `any_loop_mut` centralises the BACKREF deref of `linker.r#loop`. - match &*self.any_loop_mut() { + // `any_loop` centralises the BACKREF deref of `linker.r#loop`. + match self.any_loop() { bun_event_loop::AnyEventLoop::Js { owner } => { owner.enqueue_task_concurrent(task); } @@ -3572,10 +3580,8 @@ pub mod bv2_impl { self.transpiler_for_target(known_target).options.jsx.clone() }; let tree_shaking = self.linker.options.tree_shaking; - // SAFETY: arena (`self.graph.heap`) outlives the bundle pass; coerce the - // `&mut ParseTask` to `*mut` immediately so the `&self` borrow from - // `arena()` ends before we take `&mut self` below. - let task: *mut ParseTask = self.arena().alloc(ParseTask { + // Arena-owned; freed on heap reset. + let task: &mut ParseTask = self.arena_create(ParseTask { path: task_path, contents_or_fd: parse_task::ContentsOrFd::Contents(contents), side_effects: bun_ast::SideEffects::HasSideEffects, @@ -3589,21 +3595,20 @@ pub mod bv2_impl { known_target, ..Default::default() }); - // SAFETY: `task` was just arena-allocated above; no other references exist yet. - unsafe { - // BACKREF — lifetime erased per ParseTask::ctx convention. - (*task).ctx = Some(bun_ptr::ParentRef::from_raw_mut( + // BACKREF — lifetime erased per ParseTask::ctx convention. + // SAFETY: write provenance from `ptr::from_mut`; bundle outlives the task. + task.ctx = Some(unsafe { + bun_ptr::ParentRef::from_raw_mut( std::ptr::from_mut(self).cast::>(), - )); - (*task).task.node.next = core::ptr::null_mut(); - (*task).io_task.node.next = core::ptr::null_mut(); - } + ) + }); + task.task.node.next = core::ptr::null_mut(); + task.io_task.node.next = core::ptr::null_mut(); self.increment_scan_counter(); // Handle onLoad plugins - // SAFETY: `task` lives in the bundle-pass arena; sole reference until scheduled. - if !self.enqueue_on_load_plugin_if_needed(unsafe { &mut *task }) { + if !self.enqueue_on_load_plugin_if_needed(task) { if loader.should_copy_for_bundling() { let additional_files: &mut bun_alloc::AstVec = &mut self.graph.input_files.items_additional_files_mut() @@ -4203,22 +4208,22 @@ pub mod bv2_impl { // For `Bun.build` this is a Mini loop running on the bundler thread, so // `on_load` must land there — not on the JS plugin loop — or it will // mutate `graph` / allocate from `graph.heap` off-thread. - match self.any_loop_mut() { + match self.any_loop() { bun_event_loop::AnyEventLoop::Js { owner } => { owner.enqueue_task_concurrent( bun_event_loop::ConcurrentTask::ConcurrentTask::from_callback( std::ptr::from_mut(load), - on_load_from_js_loop_raw, + run_task_from_js_loop::, ), ); } bun_event_loop::AnyEventLoop::Mini(mini) => { // SAFETY: `load` is a valid &mut for the duration of the enqueue; - // the mini loop dispatches `on_load_mini` on the bundler thread. + // the mini loop dispatches the trampoline on the bundler thread. unsafe { mini.enqueue_task_concurrent_with_extra_ctx::>( std::ptr::from_mut(load), - on_load_mini, + run_task_mini::, core::mem::offset_of!(jsc_api::JSBundler::Load, task), ); } @@ -4228,22 +4233,22 @@ pub mod bv2_impl { pub fn on_resolve_async(&mut self, resolve: &mut jsc_api::JSBundler::Resolve) { // See `on_load_async` — must dispatch on the bundler's own loop. - match self.any_loop_mut() { + match self.any_loop() { bun_event_loop::AnyEventLoop::Js { owner } => { owner.enqueue_task_concurrent( bun_event_loop::ConcurrentTask::ConcurrentTask::from_callback( std::ptr::from_mut(resolve), - on_resolve_from_js_loop_raw, + run_task_from_js_loop::, ), ); } bun_event_loop::AnyEventLoop::Mini(mini) => { // SAFETY: `resolve` is a valid &mut for the duration of the enqueue; - // the mini loop dispatches `on_resolve_mini` on the bundler thread. + // the mini loop dispatches the trampoline on the bundler thread. unsafe { mini.enqueue_task_concurrent_with_extra_ctx::>( std::ptr::from_mut(resolve), - on_resolve_mini, + run_task_mini::, core::mem::offset_of!(jsc_api::JSBundler::Resolve, task), ); } @@ -4252,29 +4257,46 @@ pub mod bv2_impl { } } - fn on_load_mini(load: *mut jsc_api::JSBundler::Load, this: *mut BundleV2<'static>) { - // SAFETY: callback contract — `load` is the ctx passed to - // `enqueue_task_concurrent_with_extra_ctx`; `this` is the BundleV2 the - // mini loop's `tick` supplies as ParentContext. - BundleV2::on_load(unsafe { &mut *load }, unsafe { &mut *this }); + /// One-deref recovery of a raw event-loop `ctx` back into a bundler task. + trait BundlerTask: Sized { + fn bundle(&self) -> bun_ptr::ParentRef>; + fn run_with_bundle(&mut self, this: &mut BundleV2<'static>); } - fn on_resolve_mini(resolve: *mut jsc_api::JSBundler::Resolve, this: *mut BundleV2<'static>) { - // SAFETY: see `on_load_mini`. - BundleV2::on_resolve(unsafe { &mut *resolve }, unsafe { &mut *this }); + impl BundlerTask for jsc_api::JSBundler::Load { + fn bundle(&self) -> bun_ptr::ParentRef> { + self.bv2.expect("bv2") + } + fn run_with_bundle(&mut self, this: &mut BundleV2<'static>) { + BundleV2::on_load(self, this); + } } - pub(crate) fn on_load_from_js_loop(load: &mut jsc_api::JSBundler::Load) { - // SAFETY: `bv2` is a live backref set in `Load::init`. - let bv2 = unsafe { &mut *load.bv2 }; - BundleV2::on_load(load, bv2); + impl BundlerTask for jsc_api::JSBundler::Resolve { + fn bundle(&self) -> bun_ptr::ParentRef> { + self.bv2.expect("bv2") + } + fn run_with_bundle(&mut self, this: &mut BundleV2<'static>) { + BundleV2::on_resolve(self, this); + } } - fn on_load_from_js_loop_raw( - load: *mut jsc_api::JSBundler::Load, - ) -> bun_event_loop::JsResult<()> { - // SAFETY: `load` is a valid pointer set up by `from_callback`. - on_load_from_js_loop(unsafe { &mut *load }); + /// Mini-loop `(ctx, ParentContext)` trampoline — the single deref site. + fn run_task_mini(ctx: *mut T, this: *mut BundleV2<'static>) { + // SAFETY: callback contract — `ctx` is the pointer passed to + // `enqueue_task_concurrent_with_extra_ctx`; `this` is the BundleV2 the + // mini loop's `tick` supplies as ParentContext. Distinct allocations. + T::run_with_bundle(unsafe { &mut *ctx }, unsafe { &mut *this }); + } + + /// JS-loop `from_callback` trampoline — the single deref site. + fn run_task_from_js_loop(ctx: *mut T) -> bun_event_loop::JsResult<()> { + // SAFETY: `ctx` is the pointer passed to `from_callback`. + let task = unsafe { &mut *ctx }; + let bundle = task.bundle(); + // SAFETY: the bundle outlives the task, lives in a distinct allocation, + // and no other borrow of it is live on this thread. + task.run_with_bundle(unsafe { bundle.assume_mut() }); Ok(()) } @@ -4454,20 +4476,6 @@ pub mod bv2_impl { } } - pub(crate) fn on_resolve_from_js_loop(resolve: &mut jsc_api::JSBundler::Resolve) { - // SAFETY: `bv2` is a live backref set in `Resolve::init`. - let bv2 = unsafe { &mut *resolve.bv2 }; - BundleV2::on_resolve(resolve, bv2); - } - - fn on_resolve_from_js_loop_raw( - resolve: *mut jsc_api::JSBundler::Resolve, - ) -> bun_event_loop::JsResult<()> { - // SAFETY: `resolve` is a valid pointer set up by `from_callback`. - on_resolve_from_js_loop(unsafe { &mut *resolve }); - Ok(()) - } - impl<'a> BundleV2<'a> { pub fn on_resolve(resolve: &mut jsc_api::JSBundler::Resolve, this: &mut BundleV2) { // RAII guard captures `this` @@ -5389,7 +5397,7 @@ pub mod bv2_impl { // Then all the distinct CSS bundles (these are JS->CSS, not CSS->CSS) for entry_point in start.css_entry_points.keys() { - let order = crate::linker_context::find_imported_files_in_css_order::find_imported_files_in_css_order(&mut self.linker, self.graph.heap, &[*entry_point]); + let order = crate::linker_context::find_imported_files_in_css_order::find_imported_files_in_css_order(&self.linker, self.graph.heap, &[*entry_point]); let order_len = order.len() as usize; chunks.push(Chunk { entry_point: chunk::EntryPoint::new( @@ -5427,10 +5435,7 @@ pub mod bv2_impl { // Arena-owned; the // `DevServerOutput` lifetime is documented as "tied to the bundler's // arena". `alloc_slice_fill_iter` moves each `Chunk` into the bump. - let chunks: *mut [Chunk] = - std::ptr::from_mut::<[Chunk]>(self.arena().alloc_slice_fill_iter(chunks)); - // SAFETY: arena outlives this fn and the `DevServerOutput` it produces. - let chunks: &mut [Chunk] = unsafe { &mut *chunks }; + let chunks: &mut [Chunk] = self.arena().alloc_slice_fill_iter(chunks); /* arena: help_catch_memory_issues — no-op (mimalloc TLH check) */ @@ -6728,23 +6733,24 @@ pub mod bv2_impl { ) }; - // Extract raw pointers so the `&mut self` borrow from - // `transpiler_for_target` doesn't overlap `self.arena()` below. - // SAFETY: `define`/`log` live for `'a` (owned by the Transpiler / - // BACKREF set in `BundleV2::init`). - let (define_ptr, log_ptr): (*mut bun_js_parser::Define, *mut bun_ast::Log) = { + // `new_lazy_export_ast` returns an `Ast<'a>` that is appended to + // `self.graph.ast`, so `define` must be `&'a mut` — longer than the + // `&mut self` borrow. Take a raw pointer so that borrow ends here. + let (define, log) = { let transpiler = self.transpiler_for_target(target); - (&raw mut *transpiler.options.define, transpiler.log) + // `log_mut()` detaches its lifetime from the transpiler borrow. + let log = transpiler.log_mut(); + let define: *mut bun_js_parser::Define = &raw mut *transpiler.options.define; + (define, log) }; let ast_for_html_entrypoint = JSAst::init( bun_js_parser::new_lazy_export_ast( heap, - // SAFETY: `define`/`log` live for `'a` (owned by the Transpiler). - unsafe { &mut *define_ptr }, + // SAFETY: `define` is owned by the Transpiler, live for `'a`. + unsafe { &mut *define }, js_parser_options, - // SAFETY: `define`/`log` live for `'a` (owned by the Transpiler). - unsafe { &mut *log_ptr }, + log, Expr::init( E::EString { data: unique_key.into(), @@ -7693,13 +7699,11 @@ pub mod bv2_impl { // (Could implement `bun_alloc::Allocator` instead of the manual vtable.) fn free(ext_free_function: *mut c_void, _: &mut [u8], _: bun_alloc::Alignment, _: usize) { - // SAFETY: ptr was created by ExternalFreeFunctionAllocator::create - let info: &mut ExternalFreeFunctionAllocator = - unsafe { &mut *ext_free_function.cast::() }; + // SAFETY: ptr was heap-allocated by ExternalFreeFunctionAllocator::create + let this = unsafe { bun_core::heap::take(ext_free_function.cast::()) }; // SAFETY: free_callback is a valid C fn provided by plugin - unsafe { (info.free_callback)(info.context) }; - // SAFETY: info was heap-allocated in create() - drop(unsafe { bun_core::heap::take(info) }); + unsafe { (this.free_callback)(this.context) }; + // `this` drops here, releasing the allocation. } } diff --git a/src/bundler/linker_context/computeChunks.rs b/src/bundler/linker_context/computeChunks.rs index e1e9da6b7569..0f6addc99494 100644 --- a/src/bundler/linker_context/computeChunks.rs +++ b/src/bundler/linker_context/computeChunks.rs @@ -12,7 +12,7 @@ use bun_wyhash::{self, Wyhash}; use crate::bun_css; use crate::bun_fs; use crate::options::{PathTemplate, PlaceholderField}; -use crate::{BundleV2, Chunk, Index, IndexInt, LinkerContext}; +use crate::{Chunk, Index, IndexInt, LinkerContext}; // Typed SoA column accessors generated by ``. use super::find_all_imported_parts_in_js_order::find_all_imported_parts_in_js_order; @@ -56,10 +56,10 @@ pub fn compute_chunks( let entry_point_to_js_chunk_idx: &mut [u32] = temp.alloc_slice_fill_copy(this.graph.entry_points.len(), u32::MAX); - // SAFETY: `parse_graph` is a backref into `BundleV2.graph`, valid for the - // link step. Raw deref (not `this.parse_graph()`) because the loop below - // needs disjoint `&mut this.graph.*` borrows while `parse_graph` is held. - let parse_graph = unsafe { &*this.parse_graph }; + // Backref copy (not `this.parse_graph()`) because the loop below needs + // disjoint `&mut this.graph.*` borrows while `parse_graph` is held. + let parse_graph_ref = this.parse_graph.expect("parse_graph set in load()"); + let parse_graph = parse_graph_ref.get(); // `bump` is a `BackRef` into `BundleV2.graph.arena`, valid for the link step. // Hoisted so the loop can hold disjoint &mut borrows into `this.graph`. // `BundlerStyleSheet::empty()` no longer takes an arena in Rust; kept for @@ -67,8 +67,8 @@ pub fn compute_chunks( let _arena: &Arena = this.graph.arena(); // borrowck escape hatch — the SoA column slices below hold disjoint - // immutable borrows into `this.graph` while several helpers (and the BundleV2 - // back-pointer recovery) still want `&mut LinkerContext`. Split borrows + // immutable borrows into `this.graph` while the BundleV2 back-pointer + // recovery still wants `*mut LinkerContext`. Split borrows // could eventually be threaded through // `LinkerGraph` instead of laundering through a raw pointer. let this_ptr: *mut LinkerContext = this; @@ -142,13 +142,7 @@ pub fn compute_chunks( } if css_asts[source_index as usize].is_some() { - // SAFETY: see `this_ptr` note above — the helper only reads from - // `this.graph` columns disjoint from the slices we hold here. - let order = find_imported_files_in_css_order( - unsafe { &mut *this_ptr }, - temp, - &[Index::init(source_index)], - ); + let order = find_imported_files_in_css_order(this, temp, &[Index::init(source_index)]); // Create a chunk for the entry point here to ensure that the chunk is // always generated even if the resulting file is empty let hash_to_use = if !this.options.css_chunking { @@ -218,12 +212,8 @@ pub fn compute_chunks( let css_source_indices = find_imported_css_files_in_js_order(this, temp, Index::init(source_index)); if css_source_indices.len() > 0 { - // SAFETY: see `this_ptr` note above. - let order = find_imported_files_in_css_order( - unsafe { &mut *this_ptr }, - temp, - css_source_indices.slice(), - ); + let order = + find_imported_files_in_css_order(this, temp, css_source_indices.slice()); // Always use content-based hashing for CSS chunk deduplication. // This ensures that when multiple JS entry points import the @@ -536,10 +526,10 @@ pub fn compute_chunks( // single allocation into `this.unique_key_buf` afterwards. let prefix_len = chunk::UNIQUE_KEY_PREFIX_LEN; - // SAFETY: `this` points to LinkerContext which is the `linker` field of BundleV2. - // Derived from `this_ptr` (raw) so it does not reborrow `*this` here — the column - // slices below hold disjoint immutable borrows into `this.graph`. - let bv2: &mut BundleV2 = unsafe { &mut *LinkerContext::bundle_v2_ptr(this_ptr) }; + // SAFETY: `this` is the `linker` field of a live `BundleV2` that owns it and + // outlives the link step. `from_raw_mut` keeps write provenance for the two + // `assume_mut` sites below; every other use projects `&BundleV2` via `Deref`. + let bv2 = unsafe { bun_ptr::ParentRef::from_raw_mut(LinkerContext::bundle_v2_ptr(this_ptr)) }; let kinds = this.graph.files.items_entry_point_kind(); let output_paths = this.graph.entry_points.items_output_path(); // re-borrow after `find_all_imported_parts_in_js_order` released `&mut this`. @@ -583,8 +573,12 @@ pub fn compute_chunks( .flags .contains(chunk::Flags::IS_BROWSER_CHUNK_FROM_SERVER_BUILD) { + // SAFETY: the borrow ends with this statement; nothing else + // reads through `bv2` or `this` while it is live. + let bv2_mut = unsafe { bv2.assume_mut() }; chunk.template.data.clone_from( - &bv2.transpiler_for_target(Target::Browser) + &bv2_mut + .transpiler_for_target(Target::Browser) .options .entry_naming, ); @@ -604,8 +598,12 @@ pub fn compute_chunks( .flags .contains(chunk::Flags::IS_BROWSER_CHUNK_FROM_SERVER_BUILD) { + // SAFETY: the borrow ends with this statement; nothing else + // reads through `bv2` or `this` while it is live. + let bv2_mut = unsafe { bv2.assume_mut() }; chunk.template.data.clone_from( - &bv2.transpiler_for_target(Target::Browser) + &bv2_mut + .transpiler_for_target(Target::Browser) .options .chunk_naming, ); diff --git a/src/bundler/linker_context/convertStmtsForChunk.rs b/src/bundler/linker_context/convertStmtsForChunk.rs index 604c95db2a82..17cb0c8f0656 100644 --- a/src/bundler/linker_context/convertStmtsForChunk.rs +++ b/src/bundler/linker_context/convertStmtsForChunk.rs @@ -44,7 +44,7 @@ pub fn convert_stmts_for_chunk( source_index: u32, stmts: &mut StmtList, part_stmts: &[bun_ast::Stmt], - chunk: &mut Chunk, + chunk: &Chunk, bump: &Bump, wrap: WrapKind, ast: &JSAst<'_>, diff --git a/src/bundler/linker_context/findImportedFilesInCSSOrder.rs b/src/bundler/linker_context/findImportedFilesInCSSOrder.rs index 746d3a9efd36..d42bdf160a02 100644 --- a/src/bundler/linker_context/findImportedFilesInCSSOrder.rs +++ b/src/bundler/linker_context/findImportedFilesInCSSOrder.rs @@ -71,7 +71,7 @@ fn memcpy_and_reset(order: &mut Vec, wip: &mut Vec( - this: &'a mut LinkerContext, + this: &'a LinkerContext, temp_arena: &'a Arena, entry_points: &[Index], ) -> Vec { @@ -303,9 +303,7 @@ pub fn find_imported_files_in_css_order<'a>( let mut visitor = Visitor { arena, - parse_graph: bun_ptr::BackRef::from( - core::ptr::NonNull::new(this.parse_graph).expect("parse_graph set in load()"), - ), + parse_graph: this.parse_graph.expect("parse_graph set in load()"), visited: Vec::::init_capacity(16), css_asts: css_asts_slice, all_import_records: all_import_records_slice, diff --git a/src/bundler/linker_context/generateChunksInParallel.rs b/src/bundler/linker_context/generateChunksInParallel.rs index f5425b6840a1..3cf8623d0754 100644 --- a/src/bundler/linker_context/generateChunksInParallel.rs +++ b/src/bundler/linker_context/generateChunksInParallel.rs @@ -109,7 +109,7 @@ pub fn generate_chunks_in_parallel( node: ThreadPoolLib::Node::default(), callback: prepare_css_asts_for_chunk, }, - chunk: std::ptr::from_mut::(chunk), + chunk, // `PrepareCssAstTask.linker` is `*mut LinkerContext<'static>` // (raw ptr is invariant); `.cast()` erases the inner `'a` to satisfy it. linker: std::ptr::from_mut::(c).cast(), diff --git a/src/bundler/linker_context/generateCodeForFileInChunkJS.rs b/src/bundler/linker_context/generateCodeForFileInChunkJS.rs index 3260238cdc0f..d21ef8aa4814 100644 --- a/src/bundler/linker_context/generateCodeForFileInChunkJS.rs +++ b/src/bundler/linker_context/generateCodeForFileInChunkJS.rs @@ -25,7 +25,7 @@ pub fn generate_code_for_file_in_chunk_js<'r, 'src>( c: &mut LinkerContext, writer: &mut js_printer::BufferWriter, r: renamer::Renamer<'r, 'src>, - chunk: &mut Chunk, + chunk: &Chunk, part_range: PartRange, to_common_js_ref: Ref, to_esm_ref: Ref, diff --git a/src/bundler/linker_context/generateCodeForLazyExport.rs b/src/bundler/linker_context/generateCodeForLazyExport.rs index 81e82c5baefd..1297acd8ccad 100644 --- a/src/bundler/linker_context/generateCodeForLazyExport.rs +++ b/src/bundler/linker_context/generateCodeForLazyExport.rs @@ -51,9 +51,10 @@ pub fn generate_code_for_lazy_export( // Take `parts` as a raw pointer *before* the // long-lived immutable `items_css()` borrow below; re-borrowed again later as needed. let parts: *mut [Part] = this.graph.ast.items_parts_mut()[source_index as usize].as_mut_slice(); - // SAFETY: parse_graph backref; raw deref because `all_sources` is held - // across `&mut *this.log` below (split borrow). - let all_sources = unsafe { &(*this.parse_graph).input_files }.items_source(); + // Backref copy: `all_sources` is tied to this local, not to `*this`, so it can + // be held across `&mut *this.log` below (split borrow). + let parse_graph_ref = this.parse_graph.expect("parse_graph set in load()"); + let all_sources = parse_graph_ref.input_files.items_source(); let all_css_asts: &[crate::bundled_ast::CssCol] = this.graph.ast.items_css(); let maybe_css_ast: Option<&BundlerStyleSheet> = all_css_asts[source_index as usize].as_deref(); diff --git a/src/bundler/linker_context/generateCompileResultForJSChunk.rs b/src/bundler/linker_context/generateCompileResultForJSChunk.rs index 4d5f83b3db99..fc56f88ae551 100644 --- a/src/bundler/linker_context/generateCompileResultForJSChunk.rs +++ b/src/bundler/linker_context/generateCompileResultForJSChunk.rs @@ -59,20 +59,14 @@ pub unsafe fn generate_compile_result_for_js_chunk(task: *mut ThreadPoolLib::Tas } let result = { - // SAFETY: `c_ptr` / `chunk_ptr` carry mutable provenance; the disjoint-write - // contract is documented on `pending_part_range_prologue`. The `&mut` - // borrows below are scoped to the impl call so they do not overlap the - // raw slot write that follows. (Peer tasks still hold their own `&mut` - // views into the same `LinkerContext`/`Chunk` for read-only printer use — - // see the renamer caveat / SAFETY note on `unsafe impl Sync for - // Chunk` in Chunk.rs.) + // SAFETY: `c_ptr` carries mutable provenance; the disjoint-write contract is + // documented on `pending_part_range_prologue`. The borrow is scoped to the + // impl call so it does not overlap the raw slot write that follows. let c_mut: &mut LinkerContext = unsafe { &mut *c_ptr }; - // SAFETY: same mutable-provenance / disjoint-write contract as `c_ptr` above. - let chunk_mut: &mut Chunk = unsafe { &mut *chunk_ptr }; generate_compile_result_for_js_chunk_impl( &mut **worker, c_mut, - chunk_mut, + part_range.ctx.chunk, part_range.part_range, ) }; @@ -86,7 +80,7 @@ pub unsafe fn generate_compile_result_for_js_chunk(task: *mut ThreadPoolLib::Tas fn generate_compile_result_for_js_chunk_impl( worker: &mut Worker, c: &mut LinkerContext, - chunk: &mut Chunk, + chunk: bun_ptr::BackRef, part_range: PartRange, ) -> CompileResult { let _trace = bun_core::perf::trace("Bundler.generateCodeForFileInChunkJS"); @@ -162,18 +156,18 @@ fn generate_compile_result_for_js_chunk_impl( // `worker.temporary_arena` / `worker.stmt_list` borrowed `&mut` above, so // a direct shared borrow is fine. Heap is pinned; see `Worker::arena`. let worker_alloc = worker.arena.get(); - // SAFETY: split borrow of `chunk` — `generate_code_for_file_in_chunk_js` never - // touches `chunk.renamer` through its `chunk` parameter; take a raw-ptr view so borrowck doesn't - // see two overlapping `&mut chunk` borrows. - let renamer_ptr: *mut crate::bun_renamer::ChunkRenamer = core::ptr::addr_of_mut!(chunk.renamer); + // SAFETY: `chunk` is a `BackRef` built with write provenance (`new_mut`); the + // pointee is live for the link step and the callee only reads `*chunk`. + let renamer_ptr: *mut crate::bun_renamer::ChunkRenamer = + unsafe { core::ptr::addr_of_mut!((*chunk.as_ptr()).renamer) }; let result = generate_code_for_file_in_chunk_js( c, &mut buffer_writer, - // SAFETY: split borrow of `*chunk` — `renamer_ptr` aliases only - // `chunk.renamer`, which the callee never touches via its `chunk` - // parameter, so this deref does not overlap the `chunk` reborrow below. + // SAFETY: `renamer_ptr` aliases only `chunk.renamer`, which the callee + // never touches through its `chunk` parameter. See the renamer caveat on + // `unsafe impl Sync for Chunk` in Chunk.rs. unsafe { (*renamer_ptr).as_renamer() }, - chunk, + chunk.get(), part_range, to_common_js_ref, to_esm_ref, diff --git a/src/bundler/linker_context/postProcessCSSChunk.rs b/src/bundler/linker_context/postProcessCSSChunk.rs index a774121cbc39..7b48dbe531ab 100644 --- a/src/bundler/linker_context/postProcessCSSChunk.rs +++ b/src/bundler/linker_context/postProcessCSSChunk.rs @@ -14,7 +14,9 @@ pub fn post_process_css_chunk( worker: &mut thread_pool::Worker, chunk: &mut Chunk, ) -> Result<(), bun_core::Error> { - let c = ctx.c(); + // SAFETY: caller must ensure no peer `generate_chunk` task holds an + // overlapping borrow of the linker; see `GenerateChunkCtx::c`. + let c = unsafe { ctx.c() }; // Avoid FRU `..Default::default()` — StringJoiner impls Drop (E0509). let mut j = StringJoiner::default(); j.watcher = Watcher { diff --git a/src/bundler/linker_context/postProcessHTMLChunk.rs b/src/bundler/linker_context/postProcessHTMLChunk.rs index e2166e466635..095debfa4eda 100644 --- a/src/bundler/linker_context/postProcessHTMLChunk.rs +++ b/src/bundler/linker_context/postProcessHTMLChunk.rs @@ -12,7 +12,9 @@ pub fn post_process_html_chunk( // The body has no fallible sites; the Result signature matches the other // `post_process_*_chunk` callees dispatched from `generate_chunk`. // This is where we split output into pieces - let c = ctx.c(); + // SAFETY: caller must ensure no peer `generate_chunk` task holds an + // overlapping borrow of the linker; see `GenerateChunkCtx::c`. + let c = unsafe { ctx.c() }; // E0509: StringJoiner has Drop, so FRU `..Default::default()` is illegal — assign field instead. let mut j = StringJoiner::default(); j.watcher = Watcher { diff --git a/src/bundler/linker_context/postProcessJSChunk.rs b/src/bundler/linker_context/postProcessJSChunk.rs index 84b494923e8e..c73b7aba86a5 100644 --- a/src/bundler/linker_context/postProcessJSChunk.rs +++ b/src/bundler/linker_context/postProcessJSChunk.rs @@ -49,7 +49,9 @@ pub fn post_process_js_chunk( let _trace = perf::trace("Bundler.postProcessJSChunk"); let _ = chunk_index; - let c: &mut LinkerContext = ctx.c(); + // SAFETY: caller must ensure no peer `generate_chunk` task holds an + // overlapping borrow of the linker; see `GenerateChunkCtx::c`. + let c: &mut LinkerContext = unsafe { ctx.c() }; debug_assert!(matches!( chunk.content, crate::chunk::Content::Javascript(_) diff --git a/src/bundler/linker_context/prepareCssAstsForChunk.rs b/src/bundler/linker_context/prepareCssAstsForChunk.rs index 2eb15f6844ad..665cfa0ef473 100644 --- a/src/bundler/linker_context/prepareCssAstsForChunk.rs +++ b/src/bundler/linker_context/prepareCssAstsForChunk.rs @@ -20,33 +20,23 @@ use bun_resolver::DataURL; use crate::chunk::{Content, CssImportOrderKind}; -// Raw pointers rather than `&mut` / `&` so that -// (a) the container_of `container_of` recovery of `*mut BundleV2` from -// `linker` retains write provenance over the whole bundle, and (b) multiple -// tasks may hold pointers to the same `LinkerContext` concurrently without -// materializing aliased Rust references. -pub struct PrepareCssAstTask { +// `linker` stays raw so the container_of recovery of `*mut BundleV2` keeps +// write provenance over the whole bundle, and so many tasks may point at the +// same `LinkerContext` without materializing aliased Rust references. +pub struct PrepareCssAstTask<'a> { pub task: ThreadPoolLib::Task, - pub chunk: *mut Chunk, + pub chunk: &'a mut Chunk, pub linker: *mut LinkerContext<'static>, } // SAFETY: scheduled on the worker pool via raw `*mut Task` (bypassing the -// `OwnedTask: Send` route). Both raw-ptr fields point at `Send` types -// (`Chunk: Send`, `LinkerContext: Send`); the callback writes only the -// per-chunk `chunk.content.css` cell (see `prepare_css_asts_for_chunk` -// CONCURRENCY note). -unsafe impl Send for PrepareCssAstTask {} +// `OwnedTask: Send` route). `Chunk: Send` and `LinkerContext: Send`; the +// callback writes only the per-chunk `chunk.content.css` cell. +unsafe impl Send for PrepareCssAstTask<'_> {} -// CONCURRENCY: thread-pool callback — runs on worker threads, one task per -// CSS chunk. Writes: `chunk.content.css.{asts, ordered_import_records}` -// (per-chunk disjoint via `*mut Chunk`). Reads `linker.parse_graph` -// SoA columns + `linker.graph.ast.css` shared. Every CSS chunk gets exactly -// one task, so `&mut *chunk` is unique; `linker` is shared across all tasks -// and is therefore borrowed as `&LinkerContext` (the impl only reads `c` and -// only ever writes `chunk`). `PrepareCssAstTask` is `Send` by virtue of -// `LinkerContext: Send` + `Chunk: Send` (both raw-ptr fields point at types -// with `unsafe impl Send`). +// CONCURRENCY: thread-pool callback — one task per CSS chunk, so each task +// owns its chunk's `&mut`. Writes `chunk.content.css`; reads `linker` +// (`parse_graph` SoA columns + `graph.ast.css`) shared across all tasks. /// # Safety /// /// `task` must be the intrusive `task` field of a live [`PrepareCssAstTask`] @@ -55,11 +45,10 @@ unsafe impl Send for PrepareCssAstTask {} pub unsafe fn prepare_css_asts_for_chunk(task: *mut ThreadPoolLib::Task) { // SAFETY: `task` points to `PrepareCssAstTask.task` (intrusive thread-pool // node); the thread pool hands us exclusive access for the callback's - // duration. We only read the two raw-pointer fields. - let prepare_css_asts: &PrepareCssAstTask = - unsafe { &*bun_core::from_field_ptr!(PrepareCssAstTask, task, task) }; + // duration, so the typed recovery below is unique. + let prepare_css_asts: &mut PrepareCssAstTask = + unsafe { &mut *bun_core::from_field_ptr!(PrepareCssAstTask, task, task) }; let linker: *mut LinkerContext = prepare_css_asts.linker; - let chunk: *mut Chunk = prepare_css_asts.chunk; let worker = { // SAFETY: `linker` is a raw `*mut` to `BundleV2.linker` (embedded by value), // carrying provenance over the full `BundleV2` allocation. Recover the @@ -71,18 +60,19 @@ pub unsafe fn prepare_css_asts_for_chunk(task: *mut ThreadPoolLib::Task) { // SAFETY: `linker` outlives this task (owned by the bundle) and is shared // across every concurrently-running `PrepareCssAstTask`, so it must be a - // shared `&LinkerContext` — never `&mut`, which would alias across worker - // threads. Each CSS chunk gets exactly one `PrepareCssAstTask` (see - // generateChunksInParallel.rs), so `&mut *chunk` is unique. `worker.arena` - // was initialized in `Worker::create()` and points at the worker's heap - // arena. - prepare_css_asts_for_chunk_impl(unsafe { &*linker }, unsafe { &mut *chunk }, worker.arena()); + // shared `&LinkerContext` — never `&mut`, which would alias across threads. + prepare_css_asts_for_chunk_impl( + unsafe { &*linker }, + &mut *prepare_css_asts.chunk, + worker.arena(), + ); } fn prepare_css_asts_for_chunk_impl(c: &LinkerContext, chunk: &mut Chunk, bump: &Bump) { - // SAFETY: parse_graph backref; raw deref because `parse_graph` is held - // across the log write below (split borrow). - let parse_graph = unsafe { &*c.parse_graph }; + // Backref copy: the `&Graph` is tied to this local, not to `*c`, so it can be + // held across the log write below (split borrow). + let parse_graph_ref = c.parse_graph.expect("parse_graph set in load()"); + let parse_graph = parse_graph_ref.get(); let asts = c.graph.ast.items_css(); // Prepare CSS asts diff --git a/src/bundler/linker_context/writeOutputFilesToDisk.rs b/src/bundler/linker_context/writeOutputFilesToDisk.rs index 08579cf5e24a..772329ead12d 100644 --- a/src/bundler/linker_context/writeOutputFilesToDisk.rs +++ b/src/bundler/linker_context/writeOutputFilesToDisk.rs @@ -75,9 +75,14 @@ pub fn write_output_files_to_disk( let mut _max_heap_allocator_inline_source_map = MaxHeapAllocator::init(); let mut pathbuf = PathBuffer::uninit(); - // SAFETY: c points to LinkerContext which is the `linker` field of BundleV2. - let bv2: &mut BundleV2 = - unsafe { &mut *LinkerContext::bundle_v2_ptr(std::ptr::from_mut::(c)) }; + // SAFETY: `c` is the `linker` field of a live `BundleV2`; the container_of + // pointer keeps write provenance over the whole `BundleV2`, so the + // `ParentRef` may later be `assume_mut`'d. + let bv2: bun_ptr::ParentRef = unsafe { + bun_ptr::ParentRef::from_raw_mut(LinkerContext::bundle_v2_ptr(std::ptr::from_mut::< + LinkerContext, + >(c))) + }; // `code()`/`code_standalone()` take both `chunk` (an element of `chunks`) // and `chunks` as @@ -224,6 +229,9 @@ pub fn write_output_files_to_disk( .flags .contains(ChunkFlags::IS_BROWSER_CHUNK_FROM_SERVER_BUILD) { + // SAFETY: `bv2` carries write provenance and no other borrow of the + // `BundleV2` is live across this call; the `&mut` dies with the stmt. + let bv2 = unsafe { bv2.assume_mut() }; &bv2.transpiler_for_target(options::Target::Browser) .options .public_path @@ -624,9 +632,12 @@ pub fn write_output_files_to_disk( let additional_len = output_files.output_files.len() - additional_start; output_files.total_insertions += u32::try_from(additional_len).expect("int cast"); let additional_output_files = &mut output_files.output_files[additional_start..]; - // SAFETY: parse_graph backref; raw deref because `parse_graph` is held - // across `c.log_mut()` below (split borrow). - let parse_graph = unsafe { &mut *c.parse_graph }; + // Backref copy: the `&mut Graph` is tied to this local, not to `*c`, + // which `c.log_mut()` below borrows (split borrow). + let mut parse_graph_ref = c.parse_graph.expect("parse_graph set in load()"); + // SAFETY: `BundleV2.graph` is disjoint from `*c` (= `BundleV2.linker`); + // this is the only `&mut Graph` live in this scope. + let parse_graph = unsafe { parse_graph_ref.get_mut() }; debug_assert_eq!( parse_graph.additional_output_files.len(), additional_output_files.len() diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index 2ae6a1d809f2..71d623108fc0 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -762,16 +762,12 @@ impl<'a> Transpiler<'a> { merge_tsconfig_jsx_into(tsconfig, &mut self.options.jsx); } - let Some(dir) = dir_info.get_entries(self.resolver.generation) else { + // `dot_env::Loader::load` takes `impl DirEntryProbe` by shared + // reference (bun_dotenv sits below `bun_resolver` in the crate + // graph); `bun_resolver::fs::DirEntry` impls it. + let Some(dir) = dir_info.get_entries_ref(self.resolver.generation) else { return Ok(()); }; - // `get_entries` returns `*mut bun_resolver::fs::DirEntry` - // (BSSMap-owned). `dot_env::Loader::load` takes - // `impl DirEntryProbe` (bun_dotenv sits below `bun_resolver` - // in the crate graph); `bun_resolver::fs::DirEntry` impls it. - // SAFETY: BSSMap singleton owns `*dir`; single-threaded path — - // sole `&mut` for the call. - let dir: &mut bun_resolver::fs::DirEntry = unsafe { &mut *dir }; // `Env.files: Box<[Box<[u8]>]>` but `Loader::load` // wants `&[&[u8]]`. Re-borrow into a small Vec; the explicit @@ -949,7 +945,7 @@ pub struct ParseOptions<'a, 'b> { pub file_hash: Option, /// On exception, we might still want to watch the file. - pub file_fd_ptr: Option<&'b mut FD>, + pub file_fd_ptr: Option<&'b core::cell::Cell>, pub path: bun_paths::fs::Path<'static>, pub loader: options::Loader, @@ -1243,12 +1239,11 @@ impl<'a> Transpiler<'a> { // Transfer ownership of both allocations into the global // singleton via `heap::alloc` (the AtomicPtr becomes the // owner; matches `MiniEventLoop::init_global`). - let map: *mut dot_env::Map = - bun_core::heap::into_raw(Box::new(dot_env::Map::init())); - // SAFETY: `map` is a fresh heap allocation with no other - // alias; `Loader` stores it for process lifetime and is + // `Loader` stores the map for process lifetime and is // itself installed into `dot_env::INSTANCE` below. - bun_core::heap::into_raw(Box::new(dot_env::Loader::init(unsafe { &mut *map }))) + let map: &'static mut dot_env::Map = + bun_core::heap::release(Box::new(dot_env::Map::init())); + bun_core::heap::into_raw(Box::new(dot_env::Loader::init(map))) } }, }; @@ -1485,7 +1480,7 @@ impl<'a> Transpiler<'a> { }; input_fd = Some(entry.fd); if let Some(file_fd_ptr) = this_parse.file_fd_ptr { - *file_fd_ptr = entry.fd; + file_fd_ptr.set(entry.fd); } // `Source.contents: &'static [u8]` (the AST crate's `Str` // convention). The bytes live either in the per-thread shared @@ -2320,7 +2315,7 @@ impl<'a> Transpiler<'a> { format: js_printer::Format, source_map_context: Option>, runtime_transpiler_cache: Option>, - module_info: Option<*mut analyze_transpiled_module::ModuleInfo>, + module_info: Option<&mut analyze_transpiled_module::ModuleInfo>, ) -> Result { // Routed through the T0 ftrace subset like the // other bundler spans (`Bundler.computeChunks` etc.) — @@ -2535,7 +2530,7 @@ impl<'a> Transpiler<'a> { source_map_context: Option>, exports_kind: bun_ast::ExportsKind, runtime_transpiler_cache: Option>, - module_info: Option<*mut analyze_transpiled_module::ModuleInfo>, + module_info: Option<&mut analyze_transpiled_module::ModuleInfo>, ) -> Result { self.print_ast_esm_ascii::( print_arena, @@ -2563,13 +2558,9 @@ impl<'a> Transpiler<'a> { source_map_context: Option>, exports_kind: bun_ast::ExportsKind, runtime_transpiler_cache: Option, - module_info: Option<*mut analyze_transpiled_module::ModuleInfo>, + module_info: Option<&mut analyze_transpiled_module::ModuleInfo>, ) -> Result { // Both set on this (EsmAscii) arm only. - // SAFETY: `module_info` is `ModuleInfo::create`'s `heap::alloc` (or - // null); it is exclusively owned by this print call until T6 reclaims - // it after `print_with_source_map` returns. - let module_info = module_info.map(|p| unsafe { &mut *p }); let opts = js_printer::Options { bundling: false, runtime_imports: ast.runtime_imports.clone(), @@ -2657,7 +2648,7 @@ impl<'a> Transpiler<'a> { writer: &mut js_printer::BufferPrinter, format: js_printer::Format, handler: js_printer::SourceMapHandler<'_>, - module_info: Option<*mut analyze_transpiled_module::ModuleInfo>, + module_info: Option<&mut analyze_transpiled_module::ModuleInfo>, ) -> Result { // env_var feature_flag getters return `Option` // (Some(default) when unset). @@ -2706,7 +2697,7 @@ impl<'a> Transpiler<'a> { result: ParseResult, writer: &mut js_printer::BufferPrinter, format: js_printer::Format, - module_info: Option<*mut analyze_transpiled_module::ModuleInfo>, + module_info: Option<&mut analyze_transpiled_module::ModuleInfo>, ) -> Result { self.print_with_source_map_maybe::( print_arena, @@ -2881,9 +2872,8 @@ impl<'a> Transpiler<'a> { let outbase: Box<[u8]> = self.result.outbase.clone(); let output_files: Box<[options::OutputFile]> = std::mem::take(&mut self.output_files).into_boxed_slice(); - // SAFETY: see above (`self.log` is the same pointer as `log`). let mut final_result = - options::TransformResult::init(outbase, output_files, unsafe { &mut *self.log })?; + options::TransformResult::init(outbase, output_files, self.log_mut())?; // Non-owning fd view; `output_dir_handle` keeps ownership. final_result.root_dir = self .options diff --git a/src/bunfig/Cargo.toml b/src/bunfig/Cargo.toml index 217101f938a1..f5f6e21bfc0b 100644 --- a/src/bunfig/Cargo.toml +++ b/src/bunfig/Cargo.toml @@ -39,5 +39,6 @@ bun_url.workspace = true bun_ast.workspace = true bun_options_types.workspace = true bun_paths.workspace = true +bun_ptr.workspace = true bun_standalone_graph.workspace = true bun_sys.workspace = true diff --git a/src/bunfig/arguments.rs b/src/bunfig/arguments.rs index a25da143c6dd..db53b6317a88 100644 --- a/src/bunfig/arguments.rs +++ b/src/bunfig/arguments.rs @@ -11,6 +11,7 @@ use bun_options_types::command_tag::{ALWAYS_LOADS_CONFIG, Tag as CommandTag}; use bun_options_types::context::Context; use bun_paths::PathBuffer; use bun_paths::resolve_path::{self, platform}; +use bun_ptr::BackRef; use bun_standalone_graph::StandaloneModuleGraph::StandaloneModuleGraph; use crate::bunfig::Bunfig; @@ -122,9 +123,7 @@ pub fn load_config_path( } #[cold] -fn report_bunfig_load_failure(log: *mut bun_ast::Log, err: bun_core::Error) -> ! { - // SAFETY: process-global Log; see `load_bunfig` note. - let log = unsafe { &mut *log }; +fn report_bunfig_load_failure(log: BackRef, err: bun_core::Error) -> ! { if log.has_any() { let _ = log.print(std::ptr::from_mut(Output::error_writer())); Output::print_error("\n"); @@ -158,7 +157,7 @@ pub fn load_config( if let Some(path) = get_home_config_path(&mut config_buf) { if let Err(err) = load_config_path(cmd, true, path, ctx) { - report_bunfig_load_failure(ctx.log, err); + report_bunfig_load_failure(BackRef::new(ctx.log_ref()), err); } } } @@ -221,7 +220,7 @@ pub fn load_config( let config_path = ZStr::from_buf(&config_buf[..], config_path_len); if let Err(err) = load_config_path(cmd, auto_loaded, config_path, ctx) { - report_bunfig_load_failure(ctx.log, err); + report_bunfig_load_failure(BackRef::new(ctx.log_ref()), err); } Ok(()) } diff --git a/src/collections/pool.rs b/src/collections/pool.rs index 6341d620af60..e09006aebea8 100644 --- a/src/collections/pool.rs +++ b/src/collections/pool.rs @@ -800,13 +800,11 @@ mod tests { let before = drops(); let mut list: SinglyLinkedList = SinglyLinkedList::default(); for i in 0..3 { - let node = bun_core::heap::into_raw(Box::new(Node { + // Ownership moves to the list, whose `Drop` frees it. + list.prepend(bun_core::heap::release(Box::new(Node { next: ptr::null_mut(), data: MaybeUninit::new(Tracked(Box::new(i))), - })); - // SAFETY: freshly allocated and exclusively owned; ownership moves - // to the list, whose `Drop` frees it. - list.prepend(unsafe { &mut *node }); + }))); } drop(list); assert_eq!(drops(), before + 3); diff --git a/src/crash_handler/lib.rs b/src/crash_handler/lib.rs index c35b775afc0c..b7f59d8ce93e 100644 --- a/src/crash_handler/lib.rs +++ b/src/crash_handler/lib.rs @@ -318,22 +318,18 @@ pub mod debug { // ── self debug-info singleton ──────────────────────────────────────── // PORTING.md §Global mutable state: lazy debug-only singleton. RacyCell — // only called from a stopped/crashing process (lldb or the crash handler - // after `panicking` has serialized), so no concurrent access; callers - // reborrow the returned `*mut` per-access. + // after `panicking` has serialized), so no concurrent access. static SELF_DEBUG_INFO: bun_core::RacyCell> = bun_core::RacyCell::new(None); - /// NOT thread-safe. - pub fn get_self_debug_info() -> Result<*mut SelfInfo, Error> { - // SAFETY: this is debug-only and invoked from a stopped/crashing - // process (see SELF_DEBUG_INFO above), so no concurrent access. - unsafe { - let slot = &mut *SELF_DEBUG_INFO.get(); - if let Some(info) = slot { - return Ok(std::ptr::from_mut(info)); - } + /// NOT thread-safe. The returned borrow must end before the next call. + pub fn get_self_debug_info() -> Result<&'static mut SelfInfo, Error> { + // SAFETY: debug-only, invoked from a stopped/crashing process (see + // SELF_DEBUG_INFO above): no concurrent access, no live prior borrow. + let slot: &'static mut Option = unsafe { &mut *SELF_DEBUG_INFO.get() }; + if slot.is_none() { *slot = Some(SelfInfo::open()?); - Ok(std::ptr::from_mut(slot.as_mut().unwrap())) } + Ok(slot.as_mut().unwrap()) } /// Detect whether stderr supports ANSI color escapes. #[allow(dead_code)] @@ -3103,8 +3099,7 @@ mod draft { 'attempt_dump: { // Windows has issues with opening the PDB file sometimes. let debug_info = match debug::get_self_debug_info() { - // SAFETY: lazy debug-only singleton; sole `&mut` for the dump below. - Ok(d) => unsafe { &mut *d }, + Ok(d) => d, Err(err) => { // If the stderr write fails // (e.g. broken pipe), bail out entirely; don't fall through. @@ -3150,8 +3145,7 @@ mod draft { { // Assume debug symbol tooling is reliable. let debug_info = match debug::get_self_debug_info() { - // SAFETY: lazy debug-only singleton; sole `&mut` for the dump below. - Ok(d) => unsafe { &mut *d }, + Ok(d) => d, Err(err) => { let _ = writeln!( stderr, diff --git a/src/event_loop/AnyEventLoop.rs b/src/event_loop/AnyEventLoop.rs index 98b75524e59f..4a141226003c 100644 --- a/src/event_loop/AnyEventLoop.rs +++ b/src/event_loop/AnyEventLoop.rs @@ -647,17 +647,18 @@ impl EventLoopHandle { unsafe { (*self.r#loop()).unref() }; } - pub fn env(self) -> *mut DotEnvLoader<'static> { + pub fn env(self) -> BackRef> { match self { - EventLoopHandle::Js { owner } => owner.env(), - // `env` must be set — caller invariant. `env_ptr()` takes - // `&self` and returns `Option>` (mutable - // provenance). Safe via `BackRef: Deref`. + // SAFETY: the VM-owned `DotEnv::Loader` is a thread-lifetime + // singleton; it outlives every handle to the loop. + EventLoopHandle::Js { owner } => unsafe { BackRef::from_raw(owner.env()) }, + // `env` must be set — caller invariant. `env_ptr()` takes `&self` + // and returns `Option>`. EventLoopHandle::Mini(mini) => mini .env_ptr() .expect("MiniEventLoop.env unset") - .as_ptr() - .cast(), + .cast() + .into(), } } diff --git a/src/event_loop/MiniEventLoop.rs b/src/event_loop/MiniEventLoop.rs index 4c7446ce1acc..ca086fcca3c7 100644 --- a/src/event_loop/MiniEventLoop.rs +++ b/src/event_loop/MiniEventLoop.rs @@ -26,6 +26,7 @@ use bun_collections::linear_fifo::{DynamicBuffer, LinearFifo}; use bun_core::Output; use bun_dotenv::{self as dotenv, Loader as DotEnvLoader}; use bun_io::file_poll::Store as FilePollStore; +use bun_ptr::ParentRef; use bun_sys::{self as sys, Fd, Mode}; use bun_threading::UnboundedQueue; use bun_uws::Loop as UwsLoop; @@ -121,7 +122,7 @@ thread_local! { /// overlapping `&mut` to the same allocation — UB. Return the raw pointer; /// callers reborrow `&mut` for the scope they need. pub fn init_global( - env: Option<&'static mut DotEnvLoader<'static>>, + env: Option>>, cwd: Option<&[u8]>, ) -> *mut MiniEventLoop<'static> { if GLOBAL_INITIALIZED.with(|g| g.get()) { @@ -130,34 +131,13 @@ pub fn init_global( return GLOBAL.with(|g| g.get()); } let loop_ = MiniEventLoop::init(); - // §Forbidden bans `Box::leak` for `&'static`; this is a - // thread-lifetime singleton, so use `heap::alloc` (intrusive ownership) - // and store the raw pointer in the thread-local. - let global_ptr: *mut MiniEventLoop<'static> = bun_core::heap::into_raw(Box::new(loop_)); - // SAFETY: `global_ptr` was just allocated via `heap::alloc`; this thread - // holds the only reference for the duration of first-init. The `GLOBAL` - // thread-local is NOT yet published (set below, after this `&mut` is dropped), - // so neither `MiniKind::get_vm()` nor a re-entrant `init_global()` can observe - // the pointer while this exclusive borrow is live. The `&mut` is scoped to - // this function body — NOT `'static` — and ends before we publish/return the - // raw ptr. - let global = unsafe { &mut *global_ptr }; - - // `InternalLoopData::set_parent_event_loop` (typed) lives in a - // higher tier; the sys-level API is `set_parent_raw(tag, ptr)`. Tag 1 = JS, - // tag 2 = mini (`EventLoopHandle` discriminant + 1). - { - let (tag, ptr) = EventLoopHandle::init_mini(global_ptr).into_tag_ptr(); - // SAFETY: see `loop_ptr()` invariant. - unsafe { - (*global.loop_ptr()) - .internal_loop_data - .set_parent_raw(tag, ptr) - }; - } + // §Forbidden bans `Box::leak` for `&'static`; this is a thread-lifetime + // singleton, so hand the `Box` off with `heap::release` (ownership moves to + // the thread-local) and mutate through the returned `&mut`. + let global: &mut MiniEventLoop<'static> = bun_core::heap::release(Box::new(loop_)); // The process-global loader is stored as `AtomicPtr>`. - global.env = env.map(NonNull::from).or_else(|| { + global.env = env.and_then(|p| NonNull::new(p.as_mut_ptr())).or_else(|| { NonNull::new( dotenv::INSTANCE .load(core::sync::atomic::Ordering::Acquire) @@ -165,11 +145,9 @@ pub fn init_global( ) }); if global.env.is_none() { - // Thread-lifetime singletons. - let map: *mut dotenv::Map = bun_core::heap::into_raw(Box::new(dotenv::Map::init())); - // SAFETY: `map` lives for the thread (singleton); never freed. - let loader = - bun_core::heap::into_raw_nn(Box::new(DotEnvLoader::init(unsafe { &mut *map }))); + // Thread-lifetime singletons; the map's ownership passes to the loader. + let map: &'static mut dotenv::Map = bun_core::heap::release(Box::new(dotenv::Map::init())); + let loader = bun_core::heap::into_raw_nn(Box::new(DotEnvLoader::init(map))); global.env = Some(loader); } @@ -189,12 +167,22 @@ pub fn init_global( } } - // Publish the thread-local pointer only AFTER the scoped `&mut *global_ptr` - // above is no longer used — `MiniKind::get_vm()` reads `GLOBAL` without - // checking `GLOBAL_INITIALIZED`, so publishing earlier would let a callee - // re-derive a `&mut` aliasing `global` (UB). Nothing between the `&mut` - // borrow and here reads `GLOBAL` (`EventLoopHandle::init_mini`/`into_tag_ptr` - // only copy the pointer value). + let uws_loop = global.loop_ptr(); + // The coercion consumes the `&mut`, so no reference to the loop survives + // into the publish below (`MiniKind::get_vm()` re-derives from `GLOBAL`). + let global_ptr: *mut MiniEventLoop<'static> = global; + + // `InternalLoopData::set_parent_event_loop` (typed) lives in a + // higher tier; the sys-level API is `set_parent_raw(tag, ptr)`. Tag 1 = JS, + // tag 2 = mini (`EventLoopHandle` discriminant + 1). + { + let (tag, ptr) = EventLoopHandle::init_mini(global_ptr).into_tag_ptr(); + // SAFETY: see `loop_ptr()` invariant. + unsafe { (*uws_loop).internal_loop_data.set_parent_raw(tag, ptr) }; + } + + // `MiniKind::get_vm()` reads `GLOBAL` without checking `GLOBAL_INITIALIZED`, + // so publish only once the exclusive borrow has been given up. GLOBAL.with(|g| g.set(global_ptr)); GLOBAL_INITIALIZED.with(|g| g.set(true)); global_ptr @@ -447,7 +435,7 @@ impl<'a> MiniEventLoop<'a> { /// and `ctx` is non-null and outlives the queued task (intrusive node; ownership stays /// with caller). pub unsafe fn enqueue_task_concurrent_with_extra_ctx( - &mut self, + &self, ctx: *mut C, callback: fn(*mut C, *mut P), field_offset: usize, @@ -509,7 +497,7 @@ impl<'a> MiniEventLoop<'a> { bun_io::link_impl_EventLoopCtx! { Mini for MiniEventLoop<'static> => |this| { - platform_event_loop_ptr() => (*this).loop_ptr(), + platform_event_loop_ptr() => bun_ptr::ParentRef::from_raw_mut((*this).loop_ptr()), // `file_polls_raw` to avoid aliased `&mut MiniEventLoop` while `tick*` // holds `&mut self` across the re-entrant `UwsLoop::tick()` that // reaches this body. diff --git a/src/http/AsyncHTTP.rs b/src/http/AsyncHTTP.rs index acab6d6cac42..03f6e7ac8370 100644 --- a/src/http/AsyncHTTP.rs +++ b/src/http/AsyncHTTP.rs @@ -632,45 +632,48 @@ impl SingleHTTPChannel { self.cv.wait_guarded(&mut g); } } -} -fn send_sync_callback( - this: *mut SingleHTTPChannel, - async_http: *mut AsyncHTTP<'static>, - result: HTTPClientResult<'_>, -) { - // SAFETY: `async_http` is the HTTP-thread copy (inside ThreadlocalAsyncHTTP) - // and `real` was set to the caller's stack/heap AsyncHTTP before scheduling. - let async_http = unsafe { &mut *async_http }; // Note: `AsyncHTTP` is not `Copy`/`Clone` and a raw `ptr::read`/`ptr::write` // would duplicate owned fields that are later dropped on both sides; instead // enumerate every field `on_async_http_callback` (and the client path) writes // and that callers of `send_sync` can observe, moving owned values out of // the HTTP-thread copy where necessary. - if let Some(mut real) = async_http.real { - // SAFETY: `real` outlives the HTTP-thread copy by construction. - let real = unsafe { real.as_mut() }; - real.response = async_http.response; - real.request = async_http.request.take(); - real.response_headers = core::mem::take(&mut async_http.response_headers); - real.response_encoding = async_http.response_encoding; - real.err = async_http.err; - real.redirected = async_http.redirected; - real.elapsed = async_http.elapsed; - real.gzip_elapsed = async_http.gzip_elapsed; - real.state - .store(async_http.state.load(Ordering::Relaxed), Ordering::Relaxed); - real.response_buffer = async_http.response_buffer; - } - // SAFETY: `this` is the leaked `SingleHTTPChannel` from `send_sync` and is - // alive for the process lifetime; `result` borrows the HTTP-thread copy's - // response buffer, which is the caller's buffer — outlives the read in - // `send_sync`. - unsafe { - (*this).write_item(result.detach_lifetime()); + fn complete(&self, async_http: &mut AsyncHTTP<'static>, result: HTTPClientResult<'_>) { + if let Some(mut real) = async_http.real { + // SAFETY: `real` outlives the HTTP-thread copy by construction. + let real = unsafe { real.as_mut() }; + real.response = async_http.response; + real.request = async_http.request.take(); + real.response_headers = core::mem::take(&mut async_http.response_headers); + real.response_encoding = async_http.response_encoding; + real.err = async_http.err; + real.redirected = async_http.redirected; + real.elapsed = async_http.elapsed; + real.gzip_elapsed = async_http.gzip_elapsed; + real.state + .store(async_http.state.load(Ordering::Relaxed), Ordering::Relaxed); + real.response_buffer = async_http.response_buffer; + } + // SAFETY: `result` borrows the HTTP-thread copy's response buffer, which is + // the caller's buffer — it outlives the read in `send_sync`. + self.write_item(unsafe { result.detach_lifetime() }); } } +/// Typed trampoline for `HTTPClientResultCallbackFunction`: derefs both raw +/// pointers once, then hands off to a safe method. +fn send_sync_callback( + this: *mut SingleHTTPChannel, + async_http: *mut AsyncHTTP<'static>, + result: HTTPClientResult<'_>, +) { + // SAFETY: `this` is the heap `SingleHTTPChannel` from `send_sync`, alive until + // `read_item` returns; `async_http` is the HTTP-thread copy (inside + // `ThreadlocalAsyncHTTP`), owned exclusively by this thread for the call. + let (channel, async_http) = unsafe { (&*this, &mut *async_http) }; + channel.complete(async_http, result); +} + impl<'a> AsyncHTTP<'a> { pub fn send_sync(&mut self) -> Result, bun_core::Error> { crate::http_thread::init(&Default::default()); diff --git a/src/http/HTTPThread.rs b/src/http/HTTPThread.rs index e14aafb5b44f..d0a67b09efbf 100644 --- a/src/http/HTTPThread.rs +++ b/src/http/HTTPThread.rs @@ -24,9 +24,9 @@ bun_core::declare_scope!(HTTPThread_log, visible); // log /// Since configs are interned via SSLConfig.GlobalRegistry, pointer equality /// is sufficient for lookup. Each entry holds a ref on its SSLConfig. struct SslContextCacheEntry { - /// Intrusive-refcounted custom-SSL context. The cache holds one strong - /// ref (taken in `connect`); released via `ctx.deref()` on eviction. - ctx: NonNull>, + /// Intrusive-refcounted custom-SSL context. The cache adopts one strong + /// ref (in `connect`); released via `ctx.deref()` on eviction. + ctx: bun_ptr::RefPtr>, last_used_ns: u64, /// Strong ref held by the cache entry (released on eviction). _config_ref: ssl_config::SharedPtr, @@ -38,11 +38,10 @@ impl SslContextCacheEntry { /// INVARIANT: `ctx` is set once at insert (in `connect`) to a fresh /// `heap::release`-boxed `NewHttpContext` on which the cache holds one /// strong intrusive ref; it stays live until eviction's `deref` drops it. - /// The map and all callers are HTTP-thread-only, so the returned `&mut` - /// is the sole live borrow. Centralises the `Option`-style + /// `&mut self` makes the returned borrow the sole live one. Centralises the /// `(*entry.ctx.as_ptr()).…` raw deref repeated at every lookup. #[inline] - fn ctx_mut<'a>(&self) -> &'a mut NewHttpContext { + fn ctx_mut(&mut self) -> &mut NewHttpContext { // SAFETY: see INVARIANT above. unsafe { &mut *self.ctx.as_ptr() } } @@ -53,10 +52,9 @@ impl SslContextCacheEntry { /// `NewHttpContext::deref(entry.ctx.as_ptr())` open-coded at both eviction /// paths so the set-once `NonNull` is dereferenced in one place. fn release(self) { - // SAFETY: same INVARIANT as [`ctx_mut`] — `ctx` is a - // `heap::release`-boxed `NewHttpContext` on which the cache holds one - // strong ref; this `deref` is its sole release. - unsafe { NewHttpContext::::deref(self.ctx.as_ptr()) }; + // `RefPtr` has no `Drop`, so this is the sole release of the ref the + // cache adopted at insert. + self.ctx.deref(); // self.config_ref drops here (entry.config_ref.deinit()). } } @@ -386,17 +384,12 @@ impl HttpThread { self.uws_loop } - /// Mutable access to the live uSockets event loop. - /// - /// INVARIANT: `uws_loop` is set once in [`on_start`] (published via the - /// `has_awoken` Release store) and outlives the HTTP thread. The loop is a - /// separate C heap allocation disjoint from `self`. HTTP-thread-only at - /// every caller — `wakeup()` is the sole cross-thread entry and uses the - /// raw FFI call instead. Centralises the raw `&mut *self.uws_loop` - /// upgrade repeated in `process_events`. + /// Mutable access to the live uSockets event loop. The loop is a separate + /// C heap allocation disjoint from `self`, so `&mut self` is what makes + /// the returned `&mut` exclusive; `wakeup()` uses the raw FFI call. #[inline] - fn uws_loop_mut<'a>(&self) -> &'a mut uws::Loop { - // SAFETY: see INVARIANT above. + fn uws_loop_mut(&mut self) -> &mut uws::Loop { + // SAFETY: set once in `on_start` and outlives the HTTP thread. unsafe { &mut *self.uws_loop } } @@ -507,7 +500,7 @@ impl HttpThread { if let Some(entry) = custom_ssl_context_map().get_mut(&requested_config) { // Cache hit - reuse existing SSL context entry.last_used_ns = self.timer_read(); - client.set_custom_ssl_ctx(entry.ctx); + client.set_custom_ssl_ctx(entry.ctx.data); let ctx = entry.ctx_mut(); // Keepalive is now supported for custom SSL contexts return if let Some(url) = client.http_proxy.clone() { @@ -555,7 +548,9 @@ impl HttpThread { let _ = custom_ssl_context_map().put( requested_config, SslContextCacheEntry { - ctx: ctx_nn, + // SAFETY: freshly allocated with ref_count == 1; the + // cache adopts that ref and releases it in `release`. + ctx: unsafe { bun_ptr::RefPtr::adopt_ref(ctx_nn.as_ptr()) }, last_used_ns: now, // Strong ref for the cache entry; client.tls_props keeps its own. _config_ref: tls, diff --git a/src/http/h3_client/ClientSession.rs b/src/http/h3_client/ClientSession.rs index d4b0c6e8513d..6543bf179094 100644 --- a/src/http/h3_client/ClientSession.rs +++ b/src/http/h3_client/ClientSession.rs @@ -73,30 +73,26 @@ impl ClientSession { /// /// INVARIANT: `qsocket` is set by `ClientContext::connect` once /// `us_quic_connect_addr` returns and remains valid until - /// `callbacks::on_conn_close` (which sets `closed = true`). The - /// `quic::Socket` is an FFI-owned allocation distinct from `self`, so the - /// returned `&mut` does not alias `self`. HTTP-thread-only. + /// `callbacks::on_conn_close` (which sets `closed = true`). Exclusivity of + /// the returned `&mut` is enforced by `&mut self`. HTTP-thread-only. #[inline] - pub(super) fn qsocket_mut<'s>(&self) -> Option<&'s mut quic::Socket> { + pub(super) fn qsocket_mut(&mut self) -> Option<&mut quic::Socket> { // Route through the shared [`quic_socket_mut`] accessor; see INVARIANT. self.qsocket.map(|qs| quic_socket_mut(qs.as_ptr())) } - pub fn has_headroom(&self) -> bool { + pub fn has_headroom(&mut self) -> bool { if self.closed { return false; } - let Some(qs) = self.qsocket_mut() else { - return self.pending.len() < 64; - }; // After handshake every pending entry has had make_stream called, so // lsquic's n_avail_streams already accounts for them — comparing // against pending.len would double-subtract. Before handshake nothing // is counted yet, so cap optimistically at the default MAX_STREAMS. - if !self.handshake_done { + if self.qsocket.is_none() || !self.handshake_done { return self.pending.len() < 64; } - qs.streams_avail() > 0 + self.qsocket_mut().unwrap().streams_avail() > 0 } /// Queue `client` for a stream on this connection. The lsquic stream is @@ -136,8 +132,10 @@ impl ClientSession { if let crate::HTTPRequestBody::Stream(s) = &mut client.state.original_request_body { s.ended = ended; } - if let Some(qs) = stream.qstream_mut() { - encode::drain_send_body(stream, qs); + // `drain_send_body` needs `&mut Stream` and `&mut quic::Stream` at + // once; they are disjoint objects, so bypass `Stream::qstream_mut`. + if let Some(qs) = stream.qstream { + encode::drain_send_body(stream, quic_stream_mut(qs.as_ptr())); } return; } @@ -170,7 +168,7 @@ impl ClientSession { st.client = None; let request_body_done = st.request_body_done; if let Some(qs) = st.qstream_mut() { - *qs.ext::() = None; + qs.ext::().set(None); // The success path can reach here while the request body is still // being written (server responded early). FIN would be a // content-length violation; RESET_STREAM(H3_REQUEST_CANCELLED) diff --git a/src/http/h3_client/PendingConnect.rs b/src/http/h3_client/PendingConnect.rs index 7c0c0df2a8bd..6c407192c3b7 100644 --- a/src/http/h3_client/PendingConnect.rs +++ b/src/http/h3_client/PendingConnect.rs @@ -47,7 +47,7 @@ impl PendingConnect { /// every caller. Centralises the raw `(*this.pc)` upgrade repeated at /// each consume site. #[inline] - fn pc_mut<'a>(&self) -> &'a mut quic::PendingConnect { + fn pc_mut(&mut self) -> &mut quic::PendingConnect { // SAFETY: see INVARIANT above. unsafe { &mut *self.pc } } @@ -57,7 +57,7 @@ impl PendingConnect { // holds one ref from construction until Drop. `session_mut` centralises // the backref upgrade (same invariant as the other call sites below). session_mut(session).ref_(); - let self_ = Box::new(PendingConnect { + let mut self_ = Box::new(PendingConnect { session, pc, loop_ptr: l, @@ -80,7 +80,7 @@ impl PendingConnect { pub unsafe fn on_dns_resolved(this: *mut PendingConnect) { // SAFETY: `this` was heap-allocated in `register`; reclaim it so the Box drops at // end of scope — `Drop` derefs `session` and the allocation is freed. - let this = unsafe { bun_core::heap::take(this) }; + let mut this = unsafe { bun_core::heap::take(this) }; let session = this.session; // session is kept alive by the ref `this` holds for the duration of this diff --git a/src/http/h3_client/Stream.rs b/src/http/h3_client/Stream.rs index 7f58ee643d60..db8768297f42 100644 --- a/src/http/h3_client/Stream.rs +++ b/src/http/h3_client/Stream.rs @@ -63,12 +63,10 @@ impl Stream { /// /// INVARIANT: `qstream` is set in `callbacks::on_stream_open` and remains /// valid until `callbacks::on_stream_close` / `ClientSession::detach` - /// nulls it. The `quic::Stream` is an FFI-owned allocation distinct from - /// `self`, so the returned `&mut` does not alias `self`. HTTP-thread-only. + /// nulls it. HTTP-thread-only. Borrows `self` exclusively so no two live + /// `&mut quic::Stream` can be minted from one `Stream`. #[inline] - pub fn qstream_mut<'s>(&self) -> Option<&'s mut quic::Stream> { - // Route through the shared `client_session::quic_stream_mut` accessor; - // see INVARIANT above. + pub fn qstream_mut(&mut self) -> Option<&mut quic::Stream> { self.qstream .map(|qs| super::client_session::quic_stream_mut(qs.as_ptr())) } diff --git a/src/http/h3_client/callbacks.rs b/src/http/h3_client/callbacks.rs index f9d8b23bc419..6574bf7e902d 100644 --- a/src/http/h3_client/callbacks.rs +++ b/src/http/h3_client/callbacks.rs @@ -71,7 +71,7 @@ fn session_of<'a>(qs: &mut quic::Socket) -> Option<&'a mut ClientSession> { fn stream_of<'a>(s: &mut quic::Stream) -> Option<&'a mut Stream> { // Route through `client_session::stream_mut` (one centralised unsafe); // the ext slot is `Option>` — same backref invariant. - (*s.ext::()).map(|p| stream_mut(p.as_ptr())) + s.ext::().get().map(|p| stream_mut(p.as_ptr())) } pub(crate) fn register(qctx: &mut quic::Context) { @@ -168,7 +168,7 @@ extern "C" fn on_conn_close(qs: *mut quic::Socket) { extern "C" fn on_stream_open(s: *mut quic::Stream, is_client: c_int) { let s = qstream_arg(s); - *s.ext::() = None; + s.ext::().set(None); if is_client == 0 { return; } @@ -195,7 +195,7 @@ extern "C" fn on_stream_open(s: *mut quic::Stream, is_client: c_int) { // `stream` is a live element of `session.pending` — `stream_mut` // centralises that upgrade invariant. stream_mut(stream).qstream = Some(NonNull::from(&mut *s)); - *s.ext::() = NonNull::new(stream); + s.ext::().set(NonNull::new(stream)); bun_core::scoped_log!(h3_client, "stream_open"); if let Err(e) = encode::write_request(session, stream_mut(stream), s) { session.fail(stream, e); @@ -285,7 +285,7 @@ extern "C" fn on_stream_writable(s: *mut quic::Stream) { extern "C" fn on_stream_close(s: *mut quic::Stream) { let s = qstream_arg(s); let Some(stream) = stream_of(s) else { return }; - *s.ext::() = None; + s.ext::().set(None); stream.qstream = None; bun_core::scoped_log!( h3_client, diff --git a/src/http_jsc/headers_jsc.rs b/src/http_jsc/headers_jsc.rs index 50858452f3b7..965dafe206e2 100644 --- a/src/http_jsc/headers_jsc.rs +++ b/src/http_jsc/headers_jsc.rs @@ -1,7 +1,6 @@ //! JSC bridges for `bun.http.{Headers,H2Client,H3Client}`. Keeps `src/http/` //! free of JSC types. -use core::ptr::NonNull; use core::sync::atomic::Ordering; use bun_core::{StringPointer, ZigString}; @@ -19,17 +18,8 @@ pub fn from_fetch_headers( fetch_headers: Option<&FetchHeaders>, body_content_type: Option<&[u8]>, ) -> Headers { - // `FetchHeaders::{count,fast_has_,copy_to}` take `&mut self` but - // are read-only FFI shims; cast through `*mut` (matching the prior - // `link_interface!` impl which did `from_ref(h).cast_mut()`). - let h_ptr: Option<*mut FetchHeaders> = fetch_headers.map(|h| core::ptr::from_ref(h).cast_mut()); + let (mut header_count, mut buf_len) = fetch_headers.map_or((0, 0), FetchHeaders::count); - let mut header_count: u32 = 0; - let mut buf_len: u32 = 0; - if let Some(h) = h_ptr { - // SAFETY: `h` is a valid `&FetchHeaders` for the call; FFI is read-only. - unsafe { (*h).count(&mut header_count, &mut buf_len) }; - } let mut headers = Headers { entries: EntryList::default(), buf: Vec::new(), @@ -37,10 +27,8 @@ pub fn from_fetch_headers( let buf_len_before_content_type = buf_len; let needs_content_type = 'brk: { if let Some(body_ct) = body_content_type { - // SAFETY: see `count` above. - let has_ct_header = h_ptr - .map(|h| unsafe { (*h).fast_has_(HTTPHeaderName::ContentType as u8) }) - .unwrap_or(false); + let has_ct_header = + fetch_headers.is_some_and(|h| h.fast_has(HTTPHeaderName::ContentType)); if !has_ct_header { header_count += 1; buf_len += u32::try_from(body_ct.len() + b"Content-Type".len()).unwrap(); @@ -62,50 +50,50 @@ pub fn from_fetch_headers( headers.buf.reserve_exact(buf_len as usize); // SAFETY: capacity reserved above; bytes are fully initialized by copyTo / the copy below. unsafe { headers.buf.set_len(buf_len as usize) }; - // `Slice::items` returns `&mut [F]` from `&self`; the two columns are - // disjoint allocations so simultaneous access is sound, but borrowck can't see - // that. Take raw column pointers up front and slice in scoped blocks. + // `Slice::items` returns `&mut [F]` from `&self`; the two columns are disjoint + // allocations so simultaneous access is sound, but borrowck can't see that. let sliced = headers.entries.slice(); - // SAFETY: `Name`/`Value` columns are both `StringPointer`; `Slice::items_raw` - // contract is satisfied. Disjoint backing memory ⇒ no aliasing. - let names_ptr: *mut api::StringPointer = sliced.items_raw::<"name", api::StringPointer>(); - // SAFETY: same `items_raw` contract as above; `value` column is a disjoint allocation. - let values_ptr: *mut api::StringPointer = sliced.items_raw::<"value", api::StringPointer>(); + // SAFETY: `name`/`value` are disjoint columns of exactly `header_count` + // `StringPointer` slots each; `Slice::items_raw`'s contract is satisfied. + let (names, values) = unsafe { + ( + core::slice::from_raw_parts_mut( + sliced.items_raw::<"name", api::StringPointer>(), + header_count as usize, + ), + core::slice::from_raw_parts_mut( + sliced.items_raw::<"value", api::StringPointer>(), + header_count as usize, + ), + ) + }; // Zero-init so any slot `copy_to` fails to write (iterator skip, count // desync) reads as `{0, 0}` — a valid empty slice — rather than garbage. - // SAFETY: both columns hold exactly `header_count` `StringPointer` slots. - unsafe { - core::ptr::write_bytes(names_ptr, 0, header_count as usize); - core::ptr::write_bytes(values_ptr, 0, header_count as usize); - } - if let Some(h) = h_ptr { - // SAFETY: `h` is a valid `&FetchHeaders` for the call; columns sized by `count` above. - unsafe { (*h).copy_to(names_ptr, values_ptr, headers.buf.as_mut_ptr()) }; + names.fill(api::StringPointer::default()); + values.fill(api::StringPointer::default()); + + if let Some(h) = fetch_headers { + h.copy_to(names, values, &mut headers.buf); } // TODO: maybe we should send Content-Type header first instead of last? if needs_content_type { let body_ct = body_content_type.unwrap(); let ct = b"Content-Type"; - headers.buf[buf_len_before_content_type as usize..][..ct.len()].copy_from_slice(ct); - // SAFETY: header_count >= 1 (incremented above); names_ptr points to a - // live column of `header_count` slots. - unsafe { - *names_ptr.add(header_count as usize - 1) = api::StringPointer { - offset: buf_len_before_content_type, - length: u32::try_from(ct.len()).unwrap(), - }; - } + let off = buf_len_before_content_type as usize; + headers.buf[off..][..ct.len()].copy_from_slice(ct); + headers.buf[off + ct.len()..][..body_ct.len()].copy_from_slice(body_ct); - headers.buf[buf_len_before_content_type as usize + ct.len()..][..body_ct.len()] - .copy_from_slice(body_ct); - // SAFETY: see above. - unsafe { - *values_ptr.add(header_count as usize - 1) = api::StringPointer { - offset: buf_len_before_content_type + u32::try_from(ct.len()).unwrap(), - length: u32::try_from(body_ct.len()).unwrap(), - }; - } + // `header_count` was incremented for this slot above. + let last = header_count as usize - 1; + names[last] = api::StringPointer { + offset: buf_len_before_content_type, + length: u32::try_from(ct.len()).unwrap(), + }; + values[last] = api::StringPointer { + offset: buf_len_before_content_type + u32::try_from(ct.len()).unwrap(), + length: u32::try_from(body_ct.len()).unwrap(), + }; } headers @@ -118,10 +106,7 @@ pub fn from_fetch_headers( /// receives raw `StringPointer` column pointers; `bun_http_types` and /// `bun_string` both re-export the canonical `bun_core::StringPointer`, so no /// layout cast is needed. -pub fn to_fetch_headers( - this: &Headers, - global: &JSGlobalObject, -) -> JsResult> { +pub fn to_fetch_headers(this: &Headers, global: &JSGlobalObject) -> JsResult { use bun_http_types::ETag::HeaderEntryColumns; use bun_jsc::JsError; if this.entries.len() == 0 { @@ -129,18 +114,13 @@ pub fn to_fetch_headers( } let names: &[StringPointer] = this.entries.items_name(); let values: &[StringPointer] = this.entries.items_value(); - // SAFETY: `names`/`values` point into live slices of `this.entries.len()` - // entries; C++ reads exactly `count_` of each and does not retain the pointers. FetchHeaders::create( global, - // C++ side reads only; cast_mut() is safe (no mutation). - names.as_ptr().cast_mut(), - values.as_ptr().cast_mut(), - // `from_bytes` scans for - // non-ASCII and tags UTF-8; `init` would leave the buffer Latin-1 - // and mojibake any UTF-8 header value bytes ≥0x80. + names, + values, + // `from_bytes` scans for non-ASCII and tags UTF-8; `init` would leave the + // buffer Latin-1 and mojibake any UTF-8 header value bytes ≥0x80. &ZigString::from_bytes(this.buf.as_slice()), - this.entries.len() as u32, ) .ok_or(JsError::Thrown) } diff --git a/src/http_jsc/websocket_client.rs b/src/http_jsc/websocket_client.rs index b5c43240de97..76b6c50fd05d 100644 --- a/src/http_jsc/websocket_client.rs +++ b/src/http_jsc/websocket_client.rs @@ -64,7 +64,6 @@ const MAX_CLOSE_REASON: usize = MAX_CONTROL_PAYLOAD - 2; const CONTROL_HEADER_SIZE: usize = 6; #[derive(bun_ptr::CellRefCounted)] -#[ref_count(destroy = Self::deinit)] pub struct WebSocket { pub ref_count: Cell, @@ -1500,7 +1499,7 @@ impl WebSocket { secure: Option<*mut SslCtx>, proxy_tunnel: Option>, ) -> *mut Self { - let ws = bun_core::heap::into_raw(Box::new(WebSocket:: { + let mut boxed = Box::new(WebSocket:: { ref_count: Cell::new(1), tcp: Cell::new(Socket::::detached()), outgoing_websocket: Cell::new(NonNull::new(outgoing)), @@ -1528,15 +1527,15 @@ impl WebSocket { message_is_compressed: Cell::new(false), secure: Cell::new(secure), proxy_tunnel: Cell::new(proxy_tunnel), - })); - bun_core::scoped_log!(alloc, "new({}) = {:p}", Self::ALLOC_TYPE_NAME, ws); - // SAFETY: ws was just allocated via heap::alloc; no other reference exists. - let ws_ref = unsafe { &mut *ws }; + }); if let Some(params) = deflate_params { - *ws_ref.deflate.get_mut() = WebSocketDeflate::init(*params).ok(); + *boxed.deflate.get_mut() = WebSocketDeflate::init(*params).ok(); } + let ws = bun_core::heap::into_raw(boxed); + bun_core::scoped_log!(alloc, "new({}) = {:p}", Self::ALLOC_TYPE_NAME, ws); + ws } @@ -1759,43 +1758,46 @@ impl WebSocket { } } - // `deinit` is the IntrusiveRc destructor callback; not `impl Drop` because - // self is heap-allocated via heap::alloc and crosses FFI as *mut c_void. - unsafe fn deinit(this: *mut Self) { - // SAFETY: called once when ref_count hits zero - let this_ref = unsafe { &mut *this }; - this_ref.clear_data(); + // `extern "C"` entrypoint; `this` is non-null by C++ contract (see SAFETY comment below). + #[allow(clippy::not_unsafe_ptr_arg_deref)] + pub extern "C" fn memory_cost(this: *const Self) -> usize { + // SAFETY: called from C++ with a valid pointer + let this = unsafe { &*this }; + let mut cost: usize = size_of::(); + cost += this.send_buffer.try_borrow().map_or(0, |b| b.capacity()); + cost += this.receive_buffer.try_borrow().map_or(0, |b| b.capacity()); + // This is under-estimated a little, as we don't include usockets context. + cost + } +} + +// Runs from the `CellRefCounted` default destructor (`Box::from_raw`) once +// ref_count hits zero; fields drop in declaration order afterwards. +impl Drop for WebSocket { + fn drop(&mut self) { + self.clear_data(); // deflate already dropped in clear_data; this is defensive - *this_ref.deflate.get_mut() = None; - if let Some(handler) = this_ref.initial_data_handler.take() { + *self.deflate.get_mut() = None; + if let Some(handler) = self.initial_data_handler.take() { // SAFETY: the handler box was allocated via `heap::into_raw` in // init()/init_with_tunnel() and is normally freed by the queued // microtask in `InitialDataHandler::handle`; this field still // being set means that microtask has not run yet, so the box is // live and the raw field write does not alias any borrow. unsafe { core::ptr::addr_of_mut!((*handler.as_ptr()).adopted).write(None) }; - if this_ref.global_this.bun_vm().is_shutting_down() { + if self.global_this.bun_vm().is_shutting_down() { // SAFETY: same allocation as above; the VM is shutting down, so // the queued microtask can no longer run and this is the sole // remaining owner of the box. drop(unsafe { bun_core::heap::take(handler.as_ptr()) }); } } - bun_core::scoped_log!(alloc, "destroy({}) = {:p}", Self::ALLOC_TYPE_NAME, this); - // SAFETY: this was allocated via heap::alloc in init/init_with_tunnel - drop(unsafe { bun_core::heap::take(this) }); - } - - // `extern "C"` entrypoint; `this` is non-null by C++ contract (see SAFETY comment below). - #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub extern "C" fn memory_cost(this: *const Self) -> usize { - // SAFETY: called from C++ with a valid pointer - let this = unsafe { &*this }; - let mut cost: usize = size_of::(); - cost += this.send_buffer.try_borrow().map_or(0, |b| b.capacity()); - cost += this.receive_buffer.try_borrow().map_or(0, |b| b.capacity()); - // This is under-estimated a little, as we don't include usockets context. - cost + bun_core::scoped_log!( + alloc, + "destroy({}) = {:p}", + Self::ALLOC_TYPE_NAME, + core::ptr::from_ref(self) + ); } } diff --git a/src/http_jsc/websocket_client/WebSocketUpgradeClient.rs b/src/http_jsc/websocket_client/WebSocketUpgradeClient.rs index e7c63e78ba66..b6cb5c898cea 100644 --- a/src/http_jsc/websocket_client/WebSocketUpgradeClient.rs +++ b/src/http_jsc/websocket_client/WebSocketUpgradeClient.rs @@ -32,7 +32,7 @@ use bun_core::{FeatureFlags, ZBox}; use bun_core::{String as BunString, ZigStringSlice as Utf8Slice}; use bun_http::{HeaderValueIterator, Headers}; use bun_io::KeepAlive; -use bun_jsc::{JSGlobalObject, VirtualMachineRef}; +use bun_jsc::{JSGlobalObject, JsCell, VirtualMachineRef}; use bun_picohttp as picohttp; use bun_ptr::ThisPtr; use bun_uws::{self as uws, SocketHandler, SocketKind, SslCtx}; @@ -111,29 +111,28 @@ impl Drop for SslCtxOwned { /// WebSocket HTTP upgrade client, generic over `SSL`. /// /// Intrusive single-thread -/// refcount; `ref_count` field below, `ref()`/`deref()` inherent methods, `deinit` +/// refcount; `ref_count` field below, `ref()`/`deref()` inherent methods, `Drop` /// runs when count hits 0. #[derive(bun_ptr::CellRefCounted)] -#[ref_count(destroy = Self::deinit)] pub struct HTTPClient { ref_count: Cell, - tcp: Socket, + tcp: Cell>, outgoing_websocket: Option<*mut CppWebSocket>, /// Owned request bytes. Freed via `clear_input`. - input_body_buf: Vec, + input_body_buf: JsCell>, // The unsent bytes are always a suffix of `input_body_buf`; stored here as // the suffix length so we don't hold a self-referential slice. - to_send_len: usize, - headers_buf: [picohttp::Header; 128], - body: Vec, + to_send_len: Cell, + headers_buf: JsCell<[picohttp::Header; 128]>, + body: JsCell>, /// Owned NUL-terminated hostname for SNI; empty when unset. - hostname: ZBox, + hostname: JsCell, poll_ref: KeepAlive, - state: State, + state: Cell, subprotocols: StringSet, /// Proxy state (None when not using proxy) - proxy: Option, + proxy: JsCell>, /// TLS options (full SSLConfig for complete TLS customization) ssl_config: Option>, @@ -167,6 +166,21 @@ pub struct HTTPClient { // (`deref` reaching zero) or be re-entered synchronously by `tcp.close()` / // C++ callbacks; a `&mut Self` argument across either is UB under Stacked // Borrows (argument protectors / aliased `&mut`). +/// Runs when the intrusive refcount hits zero; the derived `CellRefCounted::destroy` +/// then reclaims the `heap::into_raw` allocation from `connect` as a `Box`. +impl Drop for HTTPClient { + fn drop(&mut self) { + self.clear_data(); + debug_assert!(self.tcp.get().is_detached()); + bun_core::scoped_log!( + alloc, + "destroy({}) = {:p}", + Self::TYPE_NAME, + ptr::from_mut(self) + ); + } +} + impl HTTPClient { const TYPE_NAME: &'static str = if SSL { "http.websocket_client.WebSocketUpgradeClient.NewHTTPUpgradeClient(true)" @@ -174,26 +188,11 @@ impl HTTPClient { "http.websocket_client.WebSocketUpgradeClient.NewHTTPUpgradeClient(false)" }; - /// Called by `RefCount` when the count hits zero. - /// - /// # Safety - /// `this` must be the unique remaining pointer to a `Self` allocated via - /// `heap::alloc` in `connect`. - unsafe fn deinit(this: *mut Self) { - // SAFETY: caller guarantees `this` is the unique remaining ref. - unsafe { - (*this).clear_data(); - debug_assert!((*this).tcp.is_detached()); - // allocated via heap::alloc in `connect`. - bun_core::scoped_log!(alloc, "destroy({}) = {:p}", Self::TYPE_NAME, this); - drop(bun_core::heap::take(this)); - } - } - /// Suffix of `input_body_buf` still pending write. fn to_send(&self) -> &[u8] { - let len = self.input_body_buf.len(); - &self.input_body_buf[len - self.to_send_len..] + let buf = self.input_body_buf.get(); + let len = buf.len(); + &buf[len - self.to_send_len.get()..] } /// On error, this returns null. @@ -337,36 +336,35 @@ impl HTTPClient { subprotocols }; - let client: *mut Self = bun_core::heap::into_raw(Box::new(HTTPClient:: { + // Owned `Box` for the whole pre-connect setup; only leaked to a raw + // pointer once every field is in place (see `into_raw` below). + let mut client_box = Box::new(HTTPClient:: { ref_count: Cell::new(1), - tcp: Socket::::detached(), + tcp: Cell::new(Socket::::detached()), outgoing_websocket: Some(websocket), - input_body_buf, - to_send_len: 0, - headers_buf: [picohttp::Header::ZERO; 128], - body: Vec::new(), - hostname: ZBox::default(), + input_body_buf: JsCell::new(input_body_buf), + to_send_len: Cell::new(0), + headers_buf: JsCell::new([picohttp::Header::ZERO; 128]), + body: JsCell::new(Vec::new()), + hostname: JsCell::new(ZBox::default()), poll_ref: KeepAlive::init(), - state: State::Initializing, - proxy: proxy_state, + state: Cell::new(State::Initializing), + proxy: JsCell::new(proxy_state), ssl_config: None, secure: None, expected_accept: request_result.expected_accept, offered_permessage_deflate: offer_permessage_deflate, subprotocols, - })); - bun_core::scoped_log!(alloc, "new({}) = {:p}", Self::TYPE_NAME, client); - // SAFETY: just allocated above; we hold the only ref. This `&mut` is - // used only for pre-connect setup and MUST NOT span any - // `Socket::connect_*_group` call below — those install `client` as - // socket userdata and may synchronously dispatch - // `handle_connect_error(*mut Self)`, which would - // alias this borrow under Stacked Borrows. A fresh `&mut *client` is - // re-derived after each connect call returns. - let client_ref = unsafe { &mut *client }; + }); + bun_core::scoped_log!( + alloc, + "new({}) = {:p}", + Self::TYPE_NAME, + ptr::from_ref(&*client_box) + ); // Store TLS config if provided (ownership transferred to client) - client_ref.ssl_config = ssl_config; + client_box.ssl_config = ssl_config; let display_host_: &[u8] = if using_proxy { proxy_host_slice.as_ref().unwrap().slice() @@ -376,7 +374,7 @@ impl HTTPClient { let connect_port = if using_proxy { proxy_port } else { port }; // SAFETY: `vm_ptr` is the live per-thread VM (`global.bun_vm_ptr()`). - client_ref.poll_ref.r#ref(unsafe { vm_loop_ctx(vm_ptr) }); + client_box.poll_ref.r#ref(unsafe { vm_loop_ctx(vm_ptr) }); let display_host: &[u8] = if FeatureFlags::HARDCODE_LOCALHOST_TO_127_0_0_1 && display_host_ == b"localhost" { b"127.0.0.1" @@ -387,7 +385,7 @@ impl HTTPClient { log!( "connect: ssl={}, has_ssl_config={}, using_proxy={}", SSL, - client_ref.ssl_config.is_some(), + client_box.ssl_config.is_some(), using_proxy ); @@ -415,7 +413,7 @@ impl HTTPClient { let hooks = bun_jsc::virtual_machine::runtime_hooks().expect("RuntimeHooks not installed"); 'brk: { - if let Some(config) = &client_ref.ssl_config { + if let Some(config) = &client_box.ssl_config { if config.requires_custom_request_ctx { let mut err = uws::create_bun_socket_error_t::none; // Per-VM weak cache: every `new WebSocket(wss://, {tls:{ca}})` @@ -438,14 +436,14 @@ impl HTTPClient { // trust. The C++ caller emits an `error` event on null. log!("createSSLContext failed for WebSocket: {:?}", err); // SAFETY: `vm_ptr` is the live per-thread VM. - client_ref.poll_ref.unref(unsafe { vm_loop_ctx(vm_ptr) }); - // SAFETY: `client` from heap::alloc above; sole owner. - unsafe { Self::deref(client) }; + client_box.poll_ref.unref(unsafe { vm_loop_ctx(vm_ptr) }); + // Sole owner: dropping the `Box` here runs `Drop` + // (`clear_data`) and frees the allocation. return None; }; // Owned ref; transferred to the connected WebSocket on // upgrade, freed in `deinit` if we never get that far. - client_ref.secure = Some(SslCtxOwned(ctx)); + client_box.secure = Some(SslCtxOwned(ctx)); break 'brk Some(ctx); } } @@ -456,10 +454,10 @@ impl HTTPClient { None }; - // End the setup `&mut` before connect: `connect_*_group` may - // synchronously dispatch `handle_connect_error` via the userdata - // pointer, which would alias any live `&mut Self`. - let _ = client_ref; + // Leak the box: from here on `client` is the socket-userdata pointer. + // `connect_*_group` may synchronously dispatch `handle_connect_error` + // through it, so no reference into the allocation may be live. + let client: *mut Self = bun_core::heap::into_raw(client_box); // Unix domain socket path (ws+unix:// / wss+unix://) if let Some(usp) = &unix_socket_path_slice { @@ -472,13 +470,12 @@ impl HTTPClient { false, ) { Ok(socket) => { - // SAFETY: `client` is live (refcount >= 1); re-derive a - // fresh `&mut` now that any reentrant dispatch has - // returned. Not the sole owner anymore — `client` is also - // installed as socket userdata. - let client_ref = unsafe { &mut *client }; - client_ref.tcp = socket; - if client_ref.state == State::Failed { + // SAFETY: `client` is live (refcount >= 1). Shared borrow + // only — `client` is also installed as socket userdata, so + // no `&mut Self` may be formed from here on. + let client_ref = unsafe { &*client }; + client_ref.tcp.set(socket); + if client_ref.state.get() == State::Failed { // SAFETY: `client` from heap::alloc above. unsafe { Self::deref(client) }; return None; @@ -494,12 +491,14 @@ impl HTTPClient { // in the URL (wss+unix://name/path) to verify against // a specific certificate name. if !host_slice.slice().is_empty() { - client_ref.hostname = ZBox::from_bytes(host_slice.slice()); + client_ref + .hostname + .set(ZBox::from_bytes(host_slice.slice())); } } - client_ref.tcp.timeout(120); - client_ref.state = State::Reading; + client_ref.tcp.get().timeout(120); + client_ref.state.set(State::Reading); // +1 for cpp_websocket client_ref.ref_(); return Some(client); @@ -523,13 +522,13 @@ impl HTTPClient { false, ) { Ok(sock) => { - // SAFETY: `client` is live (refcount >= 1); re-derive a fresh - // `&mut` now that any reentrant dispatch has returned. Not the - // sole owner anymore — `client` is also socket userdata. - let out = unsafe { &mut *client }; - out.tcp = sock; + // SAFETY: `client` is live (refcount >= 1). Shared borrow only + // — `client` is also installed as socket userdata, so no + // `&mut Self` may be formed from here on. + let out = unsafe { &*client }; + out.tcp.set(sock); // I don't think this case gets reached. - if out.state == State::Failed { + if out.state.get() == State::Failed { // SAFETY: `client` from heap::alloc above. unsafe { Self::deref(client) }; return None; @@ -542,12 +541,12 @@ impl HTTPClient { // dialed. For HTTPS proxy connections, that's the proxy host, // not the wss:// target. if !display_host_.is_empty() { - out.hostname = ZBox::from_bytes(display_host_); + out.hostname.set(ZBox::from_bytes(display_host_)); } } - out.tcp.timeout(120); - out.state = State::Reading; + out.tcp.get().timeout(120); + out.state.set(State::Reading); // +1 for cpp_websocket out.ref_(); Some(client) @@ -562,8 +561,8 @@ impl HTTPClient { } pub fn clear_input(&mut self) { - self.input_body_buf = Vec::new(); - self.to_send_len = 0; + self.input_body_buf.set(Vec::new()); + self.to_send_len.set(0); } pub fn clear_data(&mut self) { @@ -573,16 +572,16 @@ impl HTTPClient { self.subprotocols.clear_and_free(); self.clear_input(); - self.body = Vec::new(); + self.body.set(Vec::new()); - if !self.hostname.is_empty() { - self.hostname = ZBox::default(); + if !self.hostname.get().is_empty() { + self.hostname.set(ZBox::default()); } // Clean up proxy state. Null the field and detach the tunnel's // back-reference before deinit so that SSLWrapper shutdown callbacks // cannot re-enter clear_data() while the proxy is still reachable. - if let Some(proxy) = self.proxy.take() { + if let Some(proxy) = self.proxy.replace(None) { if let Some(tunnel) = proxy.get_tunnel() { // SAFETY: `proxy` holds a live ref on `tunnel`. unsafe { (*tunnel.as_ptr()).detach_upgrade_client() }; @@ -623,7 +622,7 @@ impl HTTPClient { } // Copy `tcp` out so no `&mut Self` spans the close. - let tcp = this.tcp; + let tcp = this.tcp.get(); // Clear the socket's ext slot before closing. `us_socket_close` on a // SEMI_SOCKET (TCP connect still in flight — the common case when // `ws.close()` is called synchronously after `new WebSocket()`) skips @@ -645,8 +644,7 @@ impl HTTPClient { tcp.close(uws::CloseCode::Failure); } if had_socket_ref { - // SAFETY: short-lived `&mut` for the field detach. - unsafe { (*this.as_ptr()).tcp.detach() }; + this.tcp.set(Socket::::detached()); // SAFETY: refcount > 1 (the +1 from `_guard` above). unsafe { Self::deref(this.as_ptr()) }; } @@ -665,7 +663,7 @@ impl HTTPClient { let this = unsafe { ThisPtr::new(this) }; // Copy `tcp` out before dispatch so nothing touches `*this` after the // FFI call (which may reenter and pop our tag). - let tcp = this.tcp; + let tcp = this.tcp.get(); // SAFETY: forwards `this` with root provenance; no `&mut Self` is live. unsafe { Self::dispatch_abrupt_close(this.as_ptr(), code) }; @@ -700,8 +698,7 @@ impl HTTPClient { bun_jsc::mark_binding!(); // SAFETY: short-lived `&mut` borrows; each ends before the next call. unsafe { (*this.as_ptr()).clear_data() }; - // SAFETY: short-lived `&mut` for the field detach; `this` is live. - unsafe { (*this.as_ptr()).tcp.detach() }; + this.tcp.set(Socket::::detached()); // SAFETY: forwards `this` with root provenance; no `&mut Self` is live. unsafe { Self::dispatch_abrupt_close(this.as_ptr(), ErrorCode::Ended) }; @@ -766,8 +763,8 @@ impl HTTPClient { // Keep the raw pointer — round-tripping through `&c_char` would // shrink provenance to 1 byte and make the CStr scan UB. let servername = unsafe { boringssl::c::SSL_get_servername(ssl_ptr, 0) }; - let hostname = if !this.hostname.is_empty() { - this.hostname.as_bytes() + let hostname = if !this.hostname.get().is_empty() { + this.hostname.get().as_bytes() } else if !servername.is_null() { // SAFETY: SSL_get_servername returns a NUL-terminated C string // owned by the SSL session; full provenance retained above. @@ -795,15 +792,14 @@ impl HTTPClient { /// Takes `ThisPtr` because `terminate` may free `this`; see `fail`. pub fn handle_open(this: ThisPtr, socket: Socket) { log!("onOpen"); - // SAFETY: short-lived `&mut` for setup; ends before any reentrant call. - let me = unsafe { &mut *this.as_ptr() }; - me.tcp = socket; + this.tcp.set(socket); - debug_assert!(!me.input_body_buf.is_empty()); - debug_assert!(me.to_send_len == 0); + debug_assert!(!this.input_body_buf.get().is_empty()); + debug_assert!(this.to_send_len.get() == 0); if SSL { - if !me.hostname.is_empty() { + let hostname = this.hostname.get(); + if !hostname.is_empty() { if let Some(handle) = socket.get_native_handle() { // SAFETY: native handle on a TLS socket is `*SSL`; live for the // open socket's lifetime. @@ -811,14 +807,14 @@ impl HTTPClient { // `configureHTTPClient` ext-method hasn't landed on // boringssl::SSL; use bun_http's helper. // SAFETY: `handle` is the live `*mut SSL` for this just-opened - // socket (uSockets never passes null); `me.hostname` is a + // socket (uSockets never passes null); `hostname` is a // NUL-terminated CString that outlives this call. bun_http::configure_http_client_with_alpn( unsafe { &mut *handle }, - if strings::is_ip_address(me.hostname.as_bytes()) { + if strings::is_ip_address(hostname.as_bytes()) { core::ptr::null() } else { - me.hostname.as_ptr() + hostname.as_ptr() }, bun_http::AlpnOffer::H1, ); @@ -827,23 +823,26 @@ impl HTTPClient { } // If using proxy, set state to proxy_handshake - if me.proxy.is_some() { - me.state = State::ProxyHandshake; + if this.proxy.get().is_some() { + this.state.set(State::ProxyHandshake); } - let wrote = socket.write(&me.input_body_buf); + let wrote = socket.write(this.input_body_buf.get()); if wrote < 0 { - // SAFETY: no `&mut Self` is live across this call (`me`'s last use is above). + // No borrow of `*this` is live across this call. + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. unsafe { Self::terminate(this.as_ptr(), ErrorCode::FailedToWrite) }; return; } - me.to_send_len = me.input_body_buf.len() - usize::try_from(wrote).expect("int cast"); + let sent = usize::try_from(wrote).expect("int cast"); + this.to_send_len.set(this.input_body_buf.get().len() - sent); } pub fn is_same_socket(&self, socket: Socket) -> bool { // `InternalSocket` has no `PartialEq`; compare native handles. - socket.get_native_handle() == self.tcp.get_native_handle() + socket.get_native_handle() == self.tcp.get().get_native_handle() } /// Takes `ThisPtr` because `socket.close()` synchronously dispatches @@ -854,14 +853,13 @@ impl HTTPClient { // For tunnel mode after successful upgrade, forward all data to the tunnel // The tunnel will decrypt and pass to the WebSocket client - if this.state == State::Done { - // SAFETY: short-lived `&mut` for the proxy borrow; ends before return. - if let Some(p) = unsafe { &mut (*this.as_ptr()).proxy } { + if this.state.get() == State::Done { + if let Some(p) = this.proxy.get() { if let Some(tunnel) = p.get_tunnel() { let tp = tunnel.as_ptr(); // Ref the tunnel to keep it alive during this call // (in case the WebSocket client closes during processing) - // SAFETY: `p` holds a live ref on `tunnel`. + // SAFETY: `proxy` holds a live ref on `tunnel`. let _g = unsafe { bun_ptr::ScopedRef::new(tp) }; // SAFETY: ref guard above keeps the tunnel live. unsafe { WebSocketProxyTunnel::receive(tp, data) }; @@ -871,8 +869,7 @@ impl HTTPClient { } if this.outgoing_websocket.is_none() { - // SAFETY: short-lived `&mut` writes; each ends before `socket.close`. - unsafe { (*this.as_ptr()).state = State::Failed }; + this.state.set(State::Failed); // SAFETY: short-lived `&mut` for clear_data; ends before `socket.close` below. unsafe { (*this.as_ptr()).clear_data() }; // No `&mut Self` is live across this call (handle_close reenters). @@ -889,60 +886,63 @@ impl HTTPClient { debug_assert!(!socket.is_shutdown()); // Handle proxy handshake response - if this.state == State::ProxyHandshake { + if this.state.get() == State::ProxyHandshake { Self::handle_proxy_response(this, socket, data); return; } // Route through proxy tunnel if TLS handshake is in progress or complete { - // SAFETY: short-lived `&mut` for the proxy borrow. - if let Some(p) = unsafe { &mut (*this.as_ptr()).proxy } { + if let Some(p) = this.proxy.get() { if let Some(tunnel) = p.get_tunnel() { - // SAFETY: `p` holds a live ref on `tunnel`. + // SAFETY: `proxy` holds a live ref on `tunnel`. unsafe { WebSocketProxyTunnel::receive(tunnel.as_ptr(), data) }; return; } } } - // SAFETY: short-lived `&mut` for body buffering; no reentrant calls in - // this region until `terminate`/`process_response` below. - let me = unsafe { &mut *this.as_ptr() }; let mut body = data; - if !me.body.is_empty() { - me.body.extend_from_slice(data); - body = &me.body; + if !this.body.get().is_empty() { + this.body.with_mut(|b| b.extend_from_slice(data)); + body = this.body.get(); } - let is_first = me.body.is_empty(); + let is_first = this.body.get().is_empty(); const HTTP_101: &[u8] = b"HTTP/1.1 101 "; if is_first && body.len() > HTTP_101.len() { // fail early if we receive a non-101 status code if !body.starts_with(HTTP_101) { - // SAFETY: `me`'s last use is above; no `&mut Self` spans this call. + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. unsafe { Self::terminate(this.as_ptr(), ErrorCode::Expected101StatusCode) }; return; } } - let response = match picohttp::Response::parse(body, &mut me.headers_buf) { + // SAFETY: `headers_buf` is scratch backing `response` for this frame + // only — no other reference to it exists, and nothing re-enters + // between here and `response`'s last read inside `process_response`. + let headers_buf = unsafe { this.headers_buf.get_mut() }; + let response = match picohttp::Response::parse(body, headers_buf) { Ok(r) => r, Err(picohttp::ParseResponseError::MalformedHttpResponse) => { - // SAFETY: `me`'s last use is above; no `&mut Self` spans this call. + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. unsafe { Self::terminate(this.as_ptr(), ErrorCode::InvalidResponse) }; return; } Err(picohttp::ParseResponseError::ShortRead) => { - if me.body.is_empty() { - me.body.extend_from_slice(data); + if this.body.get().is_empty() { + this.body.with_mut(|b| b.extend_from_slice(data)); } // ShortRead means no \r\n\r\n was found, so every byte in // `body` is part of an incomplete header — cap that, not // total bytes received (which may include pipelined // WebSocket frames once the header does complete). - if me.body.len() > bun_http::max_http_header_size() { - // SAFETY: `me`'s last use is above; no `&mut Self` spans this call. + if this.body.get().len() > bun_http::max_http_header_size() { + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. unsafe { Self::terminate(this.as_ptr(), ErrorCode::InvalidResponse) }; } return; @@ -952,8 +952,7 @@ impl HTTPClient { let bytes_read = usize::try_from(response.bytes_read).expect("int cast"); // Reshaped for borrowck — copy remain_buf out before mutating self. let remain_buf: Vec = body[bytes_read..].to_vec(); - // SAFETY: `me`'s last use is the `body` slice above (now copied out); - // no `&mut Self` spans this call. + // SAFETY: no `&mut Self` is live; `body` is already copied out. unsafe { Self::process_response(this.as_ptr(), response, &remain_buf) }; // `_guard` drops here, balancing the ref above. May free `this`. } @@ -962,45 +961,49 @@ impl HTTPClient { fn handle_proxy_response(this: ThisPtr, socket: Socket, data: &[u8]) { log!("handleProxyResponse"); - // SAFETY: short-lived `&mut` for body buffering; no reentrant calls in - // this region until `terminate` below. - let me = unsafe { &mut *this.as_ptr() }; let mut body = data; - if !me.body.is_empty() { - me.body.extend_from_slice(data); - body = &me.body; + if !this.body.get().is_empty() { + this.body.with_mut(|b| b.extend_from_slice(data)); + body = this.body.get(); } // Check for HTTP 200 response from proxy - let is_first = me.body.is_empty(); + let is_first = this.body.get().is_empty(); const HTTP_200: &[u8] = b"HTTP/1.1 200 "; const HTTP_200_ALT: &[u8] = b"HTTP/1.0 200 "; if is_first && body.len() > HTTP_200.len() { if !body.starts_with(HTTP_200) && !body.starts_with(HTTP_200_ALT) { // Proxy connection failed - // SAFETY: `me`'s last use is above; no `&mut Self` spans this call. + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. unsafe { Self::terminate(this.as_ptr(), ErrorCode::ProxyConnectFailed) }; return; } } // Parse the response to find the end of headers - let response = match picohttp::Response::parse(body, &mut me.headers_buf) { + // SAFETY: `headers_buf` is scratch backing `response` for this frame + // only — no other reference to it exists and nothing re-enters before + // `response`'s last read. + let headers_buf = unsafe { this.headers_buf.get_mut() }; + let response = match picohttp::Response::parse(body, headers_buf) { Ok(r) => r, Err(picohttp::ParseResponseError::MalformedHttpResponse) => { - // SAFETY: `me`'s last use is above; no `&mut Self` spans this call. + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. unsafe { Self::terminate(this.as_ptr(), ErrorCode::InvalidResponse) }; return; } Err(picohttp::ParseResponseError::ShortRead) => { - if me.body.is_empty() { - me.body.extend_from_slice(data); + if this.body.get().is_empty() { + this.body.with_mut(|b| b.extend_from_slice(data)); } // ShortRead means no \r\n\r\n was found, so every byte in // `body` is part of an incomplete header — cap that, not // total bytes received. - if me.body.len() > bun_http::max_http_header_size() { - // SAFETY: `me`'s last use is above; no `&mut Self` spans this call. + if this.body.get().len() > bun_http::max_http_header_size() { + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. unsafe { Self::terminate(this.as_ptr(), ErrorCode::InvalidResponse) }; } return; @@ -1010,10 +1013,12 @@ impl HTTPClient { // Proxy returned non-200 status if response.status_code != 200 { if response.status_code == 407 { - // SAFETY: `me`'s last use is above; no `&mut Self` spans this call. + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. unsafe { Self::terminate(this.as_ptr(), ErrorCode::ProxyAuthenticationRequired) }; } else { - // SAFETY: `me`'s last use is above; no `&mut Self` spans this call. + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. unsafe { Self::terminate(this.as_ptr(), ErrorCode::ProxyConnectFailed) }; } return; @@ -1026,43 +1031,52 @@ impl HTTPClient { // Reshaped for borrowck — copy remain_buf before clearing self.body. let remain_buf: Vec = body[bytes_read..].to_vec(); - // SAFETY: re-derive a fresh `&mut` after the `body` borrow above. - let me = unsafe { &mut *this.as_ptr() }; - // Clear the body buffer for WebSocket handshake - me.body.clear(); + this.body.with_mut(|b| b.clear()); // Safely unwrap proxy state - it must exist if we're in proxy_handshake state - let Some(p) = &mut me.proxy else { - // SAFETY: `me`'s last use is above; no `&mut Self` spans this call. + let Some(p) = this.proxy.get() else { + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. unsafe { Self::terminate(this.as_ptr(), ErrorCode::ProxyTunnelFailed) }; return; }; // For wss:// through proxy, we need to do TLS handshake inside the tunnel if p.is_target_https() { - // SAFETY: `me`/`p` last used above; forwards `this` with root provenance. + // SAFETY: no borrow of `*this` is live; forwards root provenance. unsafe { Self::start_proxy_tls_handshake(this.as_ptr(), socket, &remain_buf) }; return; } // For ws:// through proxy, send the WebSocket upgrade request - me.state = State::Reading; + this.state.set(State::Reading); // Use the WebSocket upgrade request from proxy state (replaces CONNECT // request buffer; old Vec is dropped here). - me.input_body_buf = p.take_websocket_request_buf().into_vec(); - me.to_send_len = 0; + let Some(request_buf) = this + .proxy + .with_mut(|p| p.as_mut().map(|p| p.take_websocket_request_buf())) + else { + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. + unsafe { Self::terminate(this.as_ptr(), ErrorCode::ProxyTunnelFailed) }; + return; + }; + this.input_body_buf.set(request_buf.into_vec()); + this.to_send_len.set(0); // Send the WebSocket upgrade request - let wrote = socket.write(&me.input_body_buf); + let wrote = socket.write(this.input_body_buf.get()); if wrote < 0 { - // SAFETY: `me`'s last use is above; no `&mut Self` spans this call. + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. unsafe { Self::terminate(this.as_ptr(), ErrorCode::FailedToWrite) }; return; } - me.to_send_len = me.input_body_buf.len() - usize::try_from(wrote).expect("int cast"); + let sent = usize::try_from(wrote).expect("int cast"); + this.to_send_len.set(this.input_body_buf.get().len() - sent); // If there's remaining data after the proxy response, process it if !remain_buf.is_empty() { @@ -1078,12 +1092,13 @@ impl HTTPClient { unsafe fn start_proxy_tls_handshake(this: *mut Self, socket: Socket, initial_data: &[u8]) { log!("startProxyTLSHandshake"); - // SAFETY: short-lived `&mut`; no reentrant calls until `terminate` below. - let me = unsafe { &mut *this }; + // SAFETY: caller contract — `this` points to a live `Self`. + let me = unsafe { &*this }; // Safely unwrap proxy state - it must exist if we're called from handle_proxy_response - let Some(p) = &mut me.proxy else { - // SAFETY: `me`'s last use is above; no `&mut Self` spans this call. + let Some(p) = me.proxy.get() else { + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. unsafe { Self::terminate(this, ErrorCode::ProxyTunnelFailed) }; return; }; @@ -1101,7 +1116,8 @@ impl HTTPClient { { Ok(t) => t, Err(_) => { - // SAFETY: `me`/`p` last used above; no `&mut Self` spans this call. + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. unsafe { Self::terminate(this, ErrorCode::ProxyTunnelFailed) }; return; } @@ -1125,21 +1141,29 @@ impl HTTPClient { { // SAFETY: release the ref taken by `init`. unsafe { WebSocketProxyTunnel::deref(tunnel.as_ptr()) }; - // SAFETY: `me`'s last use is above; no `&mut Self` spans this call. + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. unsafe { Self::terminate(this, ErrorCode::ProxyTunnelFailed) }; return; } // Reshaped for borrowck — re-borrow proxy after uses above. - // SAFETY: re-derive a fresh `&mut`. - let me = unsafe { &mut *this }; - let Some(p) = &mut me.proxy else { - // SAFETY: `me`'s last use is above; no `&mut Self` spans this call. + // SAFETY: `start` may have re-entered; re-derive a fresh shared ref. + let me = unsafe { &*this }; + let installed = me.proxy.with_mut(|p| match p { + Some(p) => { + p.set_tunnel(Some(tunnel)); + true + } + None => false, + }); + if !installed { + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. unsafe { Self::terminate(this, ErrorCode::ProxyTunnelFailed) }; return; - }; - p.set_tunnel(Some(tunnel)); - me.state = State::ProxyTlsHandshake; + } + me.state.set(State::ProxyTlsHandshake); } /// Called by WebSocketProxyTunnel when TLS handshake completes successfully @@ -1150,50 +1174,56 @@ impl HTTPClient { pub unsafe fn on_proxy_tls_handshake_complete(this: *mut Self) { log!("onProxyTLSHandshakeComplete"); - // SAFETY: short-lived `&mut`; no reentrant calls until `terminate` below. - let me = unsafe { &mut *this }; + // SAFETY: caller contract — `this` points to a live `Self`. + let me = unsafe { &*this }; // TLS handshake done - send WebSocket upgrade request through tunnel - me.state = State::Reading; + me.state.set(State::Reading); // Free the CONNECT request buffer - me.input_body_buf = Vec::new(); - me.to_send_len = 0; + me.input_body_buf.set(Vec::new()); + me.to_send_len.set(0); - // Safely unwrap proxy state and send through the tunnel - let Some(p) = &mut me.proxy else { - // SAFETY: `me`'s last use is above; no `&mut Self` spans this call. + // Take the WebSocket upgrade request from proxy state (transfers ownership). + // Store it in input_body_buf so handle_writable can retry on drain. + let Some(request_buf) = me + .proxy + .with_mut(|p| p.as_mut().map(|p| p.take_websocket_request_buf())) + else { + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. unsafe { Self::terminate(this, ErrorCode::ProxyTunnelFailed) }; return; }; - - // Take the WebSocket upgrade request from proxy state (transfers ownership). - // Store it in input_body_buf so handle_writable can retry on drain. - me.input_body_buf = p.take_websocket_request_buf().into_vec(); - if me.input_body_buf.is_empty() { - // SAFETY: `me`/`p` last used above; no `&mut Self` spans this call. + me.input_body_buf.set(request_buf.into_vec()); + if me.input_body_buf.get().is_empty() { + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. unsafe { Self::terminate(this, ErrorCode::FailedToWrite) }; return; } // Send through the tunnel (will be encrypted). Buffer any unwritten // portion in to_send so handle_writable retries when the socket drains. - if let Some(tunnel) = p.get_tunnel() { - // SAFETY: `p` holds a live ref on `tunnel`. - let wrote = - match unsafe { WebSocketProxyTunnel::write(tunnel.as_ptr(), &me.input_body_buf) } { - Ok(n) => n, - Err(_) => { - // SAFETY: `me`/`p`/`tunnel` last used above; no `&mut Self` spans this call. - unsafe { Self::terminate(this, ErrorCode::FailedToWrite) }; - return; - } - }; - me.to_send_len = me.input_body_buf.len() - wrote; - } else { - // SAFETY: `me`/`p` last used above; no `&mut Self` spans this call. + let Some(tunnel) = me.proxy.get().as_ref().and_then(|p| p.get_tunnel()) else { + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. unsafe { Self::terminate(this, ErrorCode::ProxyTunnelFailed) }; - } + return; + }; + // SAFETY: `proxy` holds a live ref on `tunnel`. + let wrote = match unsafe { + WebSocketProxyTunnel::write(tunnel.as_ptr(), me.input_body_buf.get()) + } { + Ok(n) => n, + Err(_) => { + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. + unsafe { Self::terminate(this, ErrorCode::FailedToWrite) }; + return; + } + }; + me.to_send_len.set(me.input_body_buf.get().len() - wrote); } /// Called by WebSocketProxyTunnel with decrypted data from the TLS tunnel @@ -1204,45 +1234,51 @@ impl HTTPClient { pub unsafe fn handle_decrypted_data(this: *mut Self, data: &[u8]) { log!("handleDecryptedData: {} bytes", data.len()); - // SAFETY: short-lived `&mut` for body buffering; no reentrant calls in - // this region until `terminate`/`process_response` below. - let me = unsafe { &mut *this }; + // SAFETY: caller contract — `this` points to a live `Self`. + let me = unsafe { &*this }; // Process as if it came directly from the socket let mut body = data; - if !me.body.is_empty() { - me.body.extend_from_slice(data); - body = &me.body; + if !me.body.get().is_empty() { + me.body.with_mut(|b| b.extend_from_slice(data)); + body = me.body.get(); } - let is_first = me.body.is_empty(); + let is_first = me.body.get().is_empty(); const HTTP_101: &[u8] = b"HTTP/1.1 101 "; if is_first && body.len() > HTTP_101.len() { // fail early if we receive a non-101 status code if !body.starts_with(HTTP_101) { - // SAFETY: `me`'s last use is above; no `&mut Self` spans this call. + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. unsafe { Self::terminate(this, ErrorCode::Expected101StatusCode) }; return; } } - let response = match picohttp::Response::parse(body, &mut me.headers_buf) { + // SAFETY: `headers_buf` is scratch backing `response` for this frame + // only — no other reference to it exists and nothing re-enters before + // `response`'s last read. + let headers_buf = unsafe { me.headers_buf.get_mut() }; + let response = match picohttp::Response::parse(body, headers_buf) { Ok(r) => r, Err(picohttp::ParseResponseError::MalformedHttpResponse) => { - // SAFETY: `me`'s last use is above; no `&mut Self` spans this call. + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. unsafe { Self::terminate(this, ErrorCode::InvalidResponse) }; return; } Err(picohttp::ParseResponseError::ShortRead) => { - if me.body.is_empty() { - me.body.extend_from_slice(data); + if me.body.get().is_empty() { + me.body.with_mut(|b| b.extend_from_slice(data)); } // ShortRead means no \r\n\r\n was found, so every byte in // `body` is part of an incomplete header — cap that, not // total bytes received (which may include pipelined // WebSocket frames once the header does complete). - if me.body.len() > bun_http::max_http_header_size() { - // SAFETY: `me`'s last use is above; no `&mut Self` spans this call. + if me.body.get().len() > bun_http::max_http_header_size() { + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. unsafe { Self::terminate(this, ErrorCode::InvalidResponse) }; } return; @@ -1252,8 +1288,7 @@ impl HTTPClient { let bytes_read = usize::try_from(response.bytes_read).expect("int cast"); // Reshaped for borrowck — copy remain_buf out before mutating self. let remain_buf: Vec = body[bytes_read..].to_vec(); - // SAFETY: `me`'s last use is the `body` slice above (now copied out); - // no `&mut Self` spans this call. + // SAFETY: no `&mut Self` is live; `body` is already copied out. unsafe { Self::process_response(this, response, &remain_buf) }; } @@ -1569,8 +1604,8 @@ impl HTTPClient { }; // Check if we're using a proxy tunnel (wss:// through HTTP proxy) - // SAFETY: short-lived `&mut` for the proxy borrow. - if let Some(p) = unsafe { &mut (*this).proxy } { + // SAFETY: `this` is live per caller contract; shared read of `proxy`. + if let Some(p) = unsafe { (*this).proxy.get() } { if let Some(tunnel) = p.get_tunnel() { // wss:// through HTTP proxy: use tunnel mode // For tunnel mode, the upgrade client STAYS ALIVE to forward socket data to the tunnel. @@ -1578,7 +1613,7 @@ impl HTTPClient { // The tunnel forwards decrypted data to the WebSocket client. bun_jsc::mark_binding!(); // SAFETY: short-lived reads. - let tcp = unsafe { (*this).tcp }; + let tcp = unsafe { (*this).tcp.get() }; // SAFETY: short-lived read; `this` is live per caller contract. let has_ws = unsafe { (*this).outgoing_websocket.is_some() }; if !tcp.is_closed() && has_ws { @@ -1609,8 +1644,8 @@ impl HTTPClient { }; // Switch state to connected - handle_data will forward to tunnel - // SAFETY: short-lived write. - unsafe { (*this).state = State::Done }; + // SAFETY: `this` is live per caller contract. + unsafe { (*this).state.set(State::Done) }; // SAFETY: drops the outgoing_websocket ref; no `&mut Self` is live. unsafe { Self::deref(this) }; } else if tcp.is_closed() { @@ -1636,7 +1671,7 @@ impl HTTPClient { unsafe { (*this).clear_data() }; bun_jsc::mark_binding!(); // SAFETY: short-lived reads. - let tcp = unsafe { (*this).tcp }; + let tcp = unsafe { (*this).tcp.get() }; // SAFETY: short-lived read; `this` is live per caller contract. let has_ws = unsafe { (*this).outgoing_websocket.is_some() }; if !tcp.is_closed() && has_ws { @@ -1649,8 +1684,8 @@ impl HTTPClient { let socket = tcp; // Normal mode: pass socket directly to WebSocket client - // SAFETY: short-lived `&mut` for the field detach; ends before the FFI call below. - unsafe { (*this).tcp.detach() }; + // SAFETY: `this` is live per caller contract. + unsafe { (*this).tcp.set(Socket::::detached()) }; if let uws::InternalSocket::Connected(native_socket) = socket.socket { // SAFETY: live C++ back-reference. unsafe { @@ -1695,8 +1730,8 @@ impl HTTPClient { pub fn memory_cost(&self) -> usize { let mut cost: usize = core::mem::size_of::(); - cost += self.body.capacity(); - cost += self.to_send_len; + cost += self.body.get().capacity(); + cost += self.to_send_len.get(); cost } @@ -1706,43 +1741,40 @@ impl HTTPClient { debug_assert!(this.is_same_socket(socket)); // Forward to proxy tunnel if active - // SAFETY: short-lived `&mut` for the proxy borrow. - if let Some(p) = unsafe { &mut (*this.as_ptr()).proxy } { + if let Some(p) = this.proxy.get() { if let Some(tunnel) = p.get_tunnel() { - // SAFETY: `p` holds a live ref on `tunnel`. + // SAFETY: `proxy` holds a live ref on `tunnel`. unsafe { WebSocketProxyTunnel::on_writable(tunnel.as_ptr()) }; // In .done state (after WebSocket upgrade), just handle tunnel writes - if this.state == State::Done { + if this.state.get() == State::Done { return; } // Flush any unwritten upgrade request bytes through the tunnel - if this.to_send_len == 0 { + if this.to_send_len.get() == 0 { return; } // Bumps the intrusive refcount and derefs on Drop at every // return path below. let _guard = this.ref_guard(); - // SAFETY: `p` holds a live ref on `tunnel`. + // SAFETY: `proxy` holds a live ref on `tunnel`. let wrote = match unsafe { WebSocketProxyTunnel::write(tunnel.as_ptr(), this.to_send()) } { Ok(n) => n, Err(_) => { - // SAFETY: no `&mut Self` is live across this call. + // SAFETY: `this` is the live client for this callback; `terminate` may + // free it, and no `&mut Self` is live across the call. unsafe { Self::terminate(this.as_ptr(), ErrorCode::FailedToWrite) }; return; } }; - // SAFETY: short-lived `&mut` write. - unsafe { - let to_send_len = &mut (*this.as_ptr()).to_send_len; - *to_send_len -= wrote.min(*to_send_len); - } + let remaining = this.to_send_len.get(); + this.to_send_len.set(remaining - wrote.min(remaining)); return; } } - if this.to_send_len == 0 { + if this.to_send_len.get() == 0 { return; } @@ -1757,11 +1789,8 @@ impl HTTPClient { return; } let wrote = usize::try_from(wrote).expect("int cast"); - // SAFETY: short-lived `&mut` write. - unsafe { - let to_send_len = &mut (*this.as_ptr()).to_send_len; - *to_send_len -= wrote.min(*to_send_len); - } + let remaining = this.to_send_len.get(); + this.to_send_len.set(remaining - wrote.min(remaining)); } /// Takes `ThisPtr` because `terminate` may free `this`; see `fail`. @@ -1777,16 +1806,14 @@ impl HTTPClient { /// ref and may free `this`; a `&mut self` argument would carry a Stacked /// Borrows protector that makes deallocating its referent UB. pub fn handle_connect_error(this: ThisPtr, _: Socket, _: c_int) { - // SAFETY: short-lived `&mut` for detach; ends before any reentrant call. - unsafe { (*this.as_ptr()).tcp.detach() }; + this.tcp.set(Socket::::detached()); // For the TCP socket. - if this.state == State::Reading { + if this.state.get() == State::Reading { // SAFETY: no `&mut Self` is live across this call. unsafe { Self::terminate(this.as_ptr(), ErrorCode::FailedToConnect) }; } else { - // SAFETY: short-lived write. - unsafe { (*this.as_ptr()).state = State::Failed }; + this.state.set(State::Failed); } // SAFETY: may free `this`; no `&mut Self` is live. diff --git a/src/ini/lib.rs b/src/ini/lib.rs index 194aedb197ec..1cec0e25b698 100644 --- a/src/ini/lib.rs +++ b/src/ini/lib.rs @@ -317,15 +317,7 @@ mod draft { // read off `self.arena`) to avoid overlapping &mut self borrows. let src = self.src; let mut iter = src.split(|&b| b == b'\n'); - // TODO: borrowck — `head` aliases into `self.out.data.e_object` while - // `self` is also borrowed mutably for prepare_str(). Kept as raw `*mut` - // (the underlying `E::Object` lives in the Expr Store, not on `self`). - let mut head: *mut E::Object = std::ptr::from_mut::( - self.out - .data - .e_object_mut() - .expect("Parser.out is E.Object"), - ); + let mut head = self.out.data.e_object().expect("Parser.out is E.Object"); let ropealloc = bump; @@ -379,7 +371,7 @@ mod draft { .data .e_object_mut() .expect("Parser.out is E.Object"); - let mut parent_object = match root.get_or_put_object(section, bump) { + let parent_object = match root.get_or_put_object(section, bump) { Ok(v) => v, Err(E::SetError::OutOfMemory) => return Err(AllocError), Err(E::SetError::Clobber) => { @@ -418,12 +410,10 @@ mod draft { break 'treat_as_key; } }; - head = std::ptr::from_mut::( - parent_object - .data - .e_object_mut() - .expect("get_or_put_object returns E.Object"), - ); + head = parent_object + .data + .e_object() + .expect("get_or_put_object returns E.Object"); break 'treat_as_key; } if !treat_as_key { @@ -509,9 +499,7 @@ mod draft { _ => value_raw, }; - // SAFETY: head points into self.out's E::Object tree, valid for the - // duration of parse(). - let head_ref = unsafe { &mut *head }; + let head_ref = &mut *head; if is_array { if let Some(val) = head_ref.get(key) { @@ -1833,10 +1821,8 @@ mod draft { match &expr.data { ExprData::EString(s) => { - // SAFETY: arena-backed `EString::slice` mutates only its own - // resolved-data cache; the StoreRef pointee outlives this call. - let s_mut: &mut E::EString = unsafe { &mut *s.as_ptr() }; - let pattern = s_mut.slice(bump); + let mut s = *s; + let pattern = s.slice(bump); let matcher = match create_matcher(pattern, &mut buf) { Ok(m) => m, Err(CreateMatcherError::OutOfMemory) => return Err(FromExprError::OutOfMemory), diff --git a/src/install/PackageInstaller.rs b/src/install/PackageInstaller.rs index d96dc00ba4d7..78c40524101f 100644 --- a/src/install/PackageInstaller.rs +++ b/src/install/PackageInstaller.rs @@ -408,9 +408,9 @@ impl<'a> PackageInstaller<'a> { } #[inline] - #[allow(clippy::mut_from_ref)] - pub(crate) fn lockfile_mut(&self) -> &'a mut Lockfile { - // SAFETY: BACKREF — never null; disjoint from `*self`; see `manager_mut`. + pub(crate) fn lockfile_mut(&mut self) -> &mut Lockfile { + // SAFETY: BACKREF — never null; disjoint from `*self`. `&mut self` + // makes the returned reference the only live `&mut Lockfile`. unsafe { &mut *self.lockfile } } @@ -596,10 +596,10 @@ impl<'a> PackageInstaller<'a> { loop { // `node_modules_path` (mut) and `target_node_modules_path` // (read-only) refer to the same buffer when no replacement is - // set. Derive both from a single `*mut` so the read pointer - // shares the write reference's provenance (a `*const` taken - // from `&node_modules_path` would be popped by the later - // `&mut` reborrow under stacked-borrows). + // set. Keep the read side a raw pointer taken from the local: + // under tree-borrows such a pointer survives the sibling `&mut` + // below, while a `*const` from `&node_modules_path` would be + // disabled by that `&mut`'s first write. // SAFETY: `bin::Linker::link` only reads `target_node_modules_path` and // never writes through it while `node_modules_path` is borrowed. let nm_ptr: *mut AbsPath = &raw mut node_modules_path; @@ -615,10 +615,7 @@ impl<'a> PackageInstaller<'a> { .as_ref() .map(std::ptr::from_ref::) .unwrap_or_else(|| nm_ptr.cast_const()), - // SAFETY: `nm_ptr` = `&raw mut node_modules_path` (live local); the only - // other pointer derived from it is the read-only `target_node_modules_path` - // above, which `bin::Linker::link` never writes through. - node_modules_path: unsafe { &mut *nm_ptr }, + node_modules_path: &mut node_modules_path, abs_target_buf: link_target_buf, abs_dest_buf: link_dest_buf, rel_buf: link_rel_buf, @@ -967,7 +964,7 @@ impl<'a> PackageInstaller<'a> { // pointee outlives `'a`; the packages column buffers are not freed for // the lifetime of this `PackageInstaller` (only grow, which is why // this fn exists — to re-snapshot after growth). - let packages = self.lockfile_mut().packages.slice(); + let packages = self.lockfile().packages.slice(); self.metas = bun_ptr::RawSlice::new(packages.items_meta()); self.names = bun_ptr::RawSlice::new(packages.items_name()); self.pkg_name_hashes = bun_ptr::RawSlice::new(packages.items_name_hash()); @@ -1209,9 +1206,7 @@ impl<'a> PackageInstaller<'a> { let subpath_buf_ptr: *mut PathBuffer = &raw mut self.destination_dir_subpath_buf; let destination_dir_subpath: &mut ZStr = { let alias_slice = alias.slice(string_buf!()); - // SAFETY: `subpath_buf_ptr` is the unique borrow of the field; valid for - // the lifetime of this fn body. - let buf = unsafe { &mut *subpath_buf_ptr }; + let buf = &mut self.destination_dir_subpath_buf; buf[..alias_slice.len()].copy_from_slice(alias_slice); buf[alias_slice.len()] = 0; // SAFETY: buf[alias_slice.len()] == 0 written above; pointer derives from diff --git a/src/install/PackageManager.rs b/src/install/PackageManager.rs index 1df3a691d262..33bf78849a9d 100644 --- a/src/install/PackageManager.rs +++ b/src/install/PackageManager.rs @@ -684,20 +684,15 @@ impl PackageManager { unsafe { &mut *p } } - /// Reborrow the active progress download node (`self.progress.root`-rooted). - /// Panics if no download node is active — callers gate on - /// `options.log_level.show_progress()`, which is the same condition that - /// populates `downloads_node`. Lifetime is decoupled from `&self` for the - /// same reason as [`log_mut`]: `Progress` is a stable allocation on the - /// leaked-singleton manager and callers interleave node updates with - /// disjoint `&mut self.X` field writes. + /// The active progress download node. Panics if none is active — callers + /// gate on `options.log_level.show_progress()`, which is the same condition + /// that populates `downloads_node`. #[inline] - #[allow(clippy::mut_from_ref)] - pub fn downloads_node_mut<'a>(&self) -> &'a mut ProgressNode { + pub fn downloads_node_mut(&mut self) -> &mut ProgressNode { let p = self.downloads_node.expect("downloads_node active"); - // SAFETY: `downloads_node` points into `self.progress` (BORROW_FIELD); - // `Progress` is pinned for the manager's lifetime (leaked singleton) - // and the node is set before any caller reaches this path. + // SAFETY: `downloads_node` points at `self.progress.root` or at a caller + // stack-local that outlives the install pass; `&mut self` makes the + // reborrow exclusive. unsafe { &mut *p } } @@ -913,11 +908,13 @@ impl PackageManager { // process-lifetime `Box<[u8]>`) is encapsulated in `bun_dotenv`, not at // every call site (PORTING.md §Forbidden: never mint `'static` from // a borrowed reference). - self.env_mut().get_http_proxy_for(url) + // SAFETY: `&mut self`; no other loader borrow is live in this scope. + unsafe { self.env_mut() }.get_http_proxy_for(url) } pub fn tls_reject_unauthorized(&mut self) -> bool { - self.env_mut().get_tls_reject_unauthorized() + // SAFETY: `&mut self`; no other loader borrow is live in this scope. + unsafe { self.env_mut() }.get_tls_reject_unauthorized() } pub fn compute_is_continuous_integration(&self) -> bool { @@ -1067,19 +1064,18 @@ impl PackageManager { } /// Reborrow the process-global env loader. /// - /// Lifetime is decoupled from `&self` for the same reason as [`log_mut`] / - /// [`downloads_node_mut`]: the loader is a singleton-leaked allocation - /// outside the manager (set once in `init()`), and callers interleave env - /// mutation with disjoint `&mut self.X` field writes (e.g. `find_commit` - /// takes `env`, `log`, and reads `lockfile` in the same argument list). + /// # Safety + /// The loader is a process-lifetime singleton living outside the manager + /// and aliased by `Transpiler::env` (see `configure_env_for_scripts_run`), + /// so `&mut self` cannot establish exclusivity over it. The caller must + /// ensure no other `&mut` to the same loader is live for the duration of + /// the returned borrow — including one obtained via `Transpiler::env_mut` + /// — and must call only from the install main thread. #[inline] #[allow(clippy::mut_from_ref)] - pub fn env_mut<'a>(&self) -> &'a mut dot_env::Loader<'static> { - // SAFETY: `env` is set during `init()` and never None afterward; the - // pointee is a process-lifetime singleton (leaked `DotEnv.Loader`) - // that lives outside `self`, so the unbounded `'a` is sound under the - // same single-threaded contract as `log_mut`/`scripts_node_mut`. - // `BackRef` guarantees liveness; exclusivity is the caller's contract. + pub unsafe fn env_mut(&self) -> &mut dot_env::Loader<'static> { + // SAFETY: `env` is set during `init()` and never None afterward; + // `BackRef` guarantees the singleton outlives `self`. unsafe { &mut *self.env.expect("env initialised").as_ptr() } } } @@ -1125,7 +1121,10 @@ fn configure_env_for_scripts_run( // `Ok` — same contract as the runtime impl (run_command.rs:628). let this_transpiler = unsafe { this_transpiler_slot.assume_init() }; - let init_cwd_entry = this.env_mut().map.get_or_put_without_value(b"INIT_CWD")?; + // SAFETY: sole live loader borrow; `this_transpiler` is not touched here. + let init_cwd_entry = unsafe { this.env_mut() } + .map + .get_or_put_without_value(b"INIT_CWD")?; if !init_cwd_entry.found_existing { *init_cwd_entry.value_ptr = dot_env::HashTableValue { value: Box::<[u8]>::from(strings::without_trailing_slash( @@ -1138,7 +1137,9 @@ fn configure_env_for_scripts_run( // The resolver-tier // `FileSystem` mirrors `bun_paths::fs::FileSystem` for `top_level_dir`. let paths_fs = bun_paths::fs::FileSystem::instance(); - this.env_mut().load_ccache_path(paths_fs); + // SAFETY: sole live loader borrow; the `this_transpiler` borrow below is + // confined to its own block and does not overlap this statement. + unsafe { this.env_mut() }.load_ccache_path(paths_fs); { // Run node-gyp jobs in parallel. @@ -1159,10 +1160,13 @@ fn configure_env_for_scripts_run( { let mut node_path = PathBuffer::uninit(); - if let Some(node_path_z) = this.env_mut().get_node_path(paths_fs, &mut node_path) { - let _ = this - .env_mut() - .load_node_js_config(paths_fs, node_path_z.as_ref())?; + // SAFETY: `get_node_path` borrows `node_path`, not the loader, and the + // scrutinee temporary is dropped before the body (edition 2024). + if let Some(node_path_z) = unsafe { this.env_mut() }.get_node_path(paths_fs, &mut node_path) + { + // SAFETY: the scrutinee's loader borrow has ended. + let _ = + unsafe { this.env_mut() }.load_node_js_config(paths_fs, node_path_z.as_ref())?; } else { 'brk: { let current_path = this.env().get(b"PATH").unwrap_or(b""); @@ -1174,8 +1178,10 @@ fn configure_env_for_scripts_run( { break 'brk; } - this.env_mut().map.put(b"PATH", &path_var)?; - let _ = this.env_mut().load_node_js_config(paths_fs, bun_path)?; + // SAFETY: sequential; each loader borrow ends at its statement. + unsafe { this.env_mut() }.map.put(b"PATH", &path_var)?; + // SAFETY: same — the borrow above ended at its statement. + let _ = unsafe { this.env_mut() }.load_node_js_config(paths_fs, bun_path)?; } } } @@ -1286,7 +1292,8 @@ fn ensure_temp_node_gyp_script_run(manager: &mut PackageManager) -> Result<(), E path_var.extend_from_slice(strings::without_trailing_slash(tempdir.name)); path_var.push(SEP); path_var.extend_from_slice(&manager.node_gyp_tempdir_name); - manager.env_mut().map.put(b"PATH", &path_var)?; + // SAFETY: `manager: &mut PackageManager`; sole live loader borrow. + unsafe { manager.env_mut() }.map.put(b"PATH", &path_var)?; let path_buf_len = path_buf.len(); let mut cursor = &mut path_buf[..]; @@ -1305,8 +1312,8 @@ fn ensure_temp_node_gyp_script_run(manager: &mut PackageManager) -> Result<(), E let npm_config_node_gyp = &path_buf[..written]; let node_gyp_abs_dir = bun_core::dirname(npm_config_node_gyp).unwrap(); - manager - .env_mut() + // SAFETY: `manager: &mut PackageManager`; sole live loader borrow. + unsafe { manager.env_mut() } .map .put_alloc_key_and_value(b"BUN_WHICH_IGNORE_CWD", node_gyp_abs_dir)?; @@ -1804,15 +1811,11 @@ pub fn init( // Returns the resolver's BSSMap-owned // `*EntriesOption` slot. - let entries_option = match fs.read_directory(fs.top_level_dir(), 0, true)? { - fs::EntriesOption::Entries(e) => { - // SAFETY: the BSSMap singleton owns `*e` for the process - // lifetime, and `init()` runs single-threaded before any other - // access — sole exclusive borrow is sound. - unsafe { &mut *std::ptr::from_mut::(*e) } - } - fs::EntriesOption::Err(e) => return Err(e.canonical_error), - }; + let entries_slot = fs.read_directory(fs.top_level_dir(), 0, true)?; + if let fs::EntriesOption::Err(e) = &*entries_slot { + return Err(e.canonical_error); + } + let entries_option: &'static mut fs::DirEntry = entries_slot.entries_mut(); // SAFETY: `init()` runs once on the main thread before any other access to the singleton. // `dot_env::Loader<'a>` borrows `&'a mut Map`, so the pair is self-referential; allocate @@ -1831,8 +1834,7 @@ pub fn init( // Reborrow the BSSMap-owned `*DirEntry` for the // call; `env.load` only reads it (`hasComptimeQuery` lookups for `.env*`). env.load( - // SAFETY: see `entries_option` above — single-threaded init, BSSMap-owned. - unsafe { &mut *std::ptr::from_mut::(entries_option) }, + &*entries_option, &[], dot_env::DotEnvFileSuffix::Production, false, @@ -2319,12 +2321,11 @@ pub(crate) fn init_with_runtime_once( // leaves `holder::RAW_PTR` null rather than pointing at an uninitialized // manager. Returns the resolver's BSSMap-owned `*EntriesOption` slot. let fs_instance = FileSystem::instance(); - let root_dir = match fs_instance.read_directory(fs_instance.top_level_dir(), 0, true)? { - // SAFETY: the BSSMap singleton owns `*e` for the process lifetime, - // and runtime init runs once on the main thread before any other access. - fs::EntriesOption::Entries(e) => unsafe { &mut *std::ptr::from_mut::(*e) }, - fs::EntriesOption::Err(e) => return Err(e.canonical_error), - }; + let root_dir_slot = fs_instance.read_directory(fs_instance.top_level_dir(), 0, true)?; + if let fs::EntriesOption::Err(e) = &*root_dir_slot { + return Err(e.canonical_error); + } + let root_dir: &'static mut fs::DirEntry = root_dir_slot.entries_mut(); let cpu_count: u32 = u32::from(bun_core::get_thread_count()); allocate_package_manager(); diff --git a/src/install/PackageManager/PackageManagerDirectories.rs b/src/install/PackageManager/PackageManagerDirectories.rs index 632dc57d07c2..f82853e7e4c9 100644 --- a/src/install/PackageManager/PackageManagerDirectories.rs +++ b/src/install/PackageManager/PackageManagerDirectories.rs @@ -387,9 +387,9 @@ unsafe fn ensure_cache_directory(this: *mut PackageManager) -> Dir { // root; see fn safety contract. Project `enable` narrowly so callers // may hold borrows into disjoint `options` sub-fields. if unsafe { (*this).options.enable.contains(Enable::CACHE) } { - // SAFETY: caller-provided provenance root; `env_mut()` itself - // encapsulates the BackRef deref + singleton-liveness invariant. - let env = unsafe { &*this }.env_mut(); + // SAFETY: caller-provided provenance root; no other `&mut` to the + // singleton loader is live here (install main thread). + let env = unsafe { (*this).env_mut() }; // SAFETY: shared read of `options`; disjoint from `cache_directory_path`. let cache_dir = fetch_cache_directory_path(env, Some(unsafe { &(*this).options })); // SAFETY: see fn safety contract. diff --git a/src/install/PackageManager/PackageManagerEnqueue.rs b/src/install/PackageManager/PackageManagerEnqueue.rs index 6ed604d71ba9..309e6f45861e 100644 --- a/src/install/PackageManager/PackageManagerEnqueue.rs +++ b/src/install/PackageManager/PackageManagerEnqueue.rs @@ -511,20 +511,18 @@ pub fn enqueue_dependency_to_root( struct Closure { err: Option, - // raw `*mut` — `sleep_until` - // also receives this pointer, so `&mut` here would alias. - manager: *mut PackageManager, + manager: bun_ptr::ParentRef, } impl Closure { fn is_done(&mut self) -> bool { - // SAFETY: `self.manager` is the raw provenance root set - // below; `sleep_until`/`tick_raw` hold no `&mut` across - // this callback, so this is the unique live borrow. - let manager = unsafe { &mut *self.manager }; - if manager.pending_task_count() > 0 { + if self.manager.pending_task_count() > 0 { // All callbacks void: `VoidRunTasksCallbacks` (below) // has `Ctx = ()` and every `HAS_* = false`. - let log_level = manager.options.log_level; + let log_level = self.manager.options.log_level; + // SAFETY: `sleep_until`/`tick_raw` hold no borrow across + // this callback; the `&mut` is moved into `run_tasks` and + // is dead before any output or further read below. + let manager = unsafe { self.manager.assume_mut() }; if let Err(err) = run_tasks::run_tasks::( manager, &mut (), @@ -535,17 +533,17 @@ pub fn enqueue_dependency_to_root( return true; } - if verbose_install() && manager.pending_task_count() > 0 { + if verbose_install() && self.manager.pending_task_count() > 0 { if PackageManager::has_enough_time_passed_between_waiting_messages() { bun_core::pretty_errorln!( "[PackageManager] waiting for {} tasks\n", - manager.pending_task_count() + self.manager.pending_task_count() ); } } } - manager.pending_task_count() == 0 + self.manager.pending_task_count() == 0 } } @@ -556,12 +554,12 @@ pub fn enqueue_dependency_to_root( let mgr: *mut PackageManager = this; let mut closure = Closure { err: None, - manager: mgr, + // SAFETY: `mgr` derives from the live exclusive `this` borrow, so + // it carries the write provenance `assume_mut` requires. + manager: unsafe { bun_ptr::ParentRef::from_raw_mut(mgr) }, }; - // SAFETY: `mgr` derived from the live exclusive `this` borrow; - // `sleep_until` + `tick_raw` hold no `&mut PackageManager` across - // `Closure::is_done`, so the callback's `&mut *closure.manager` - // is the unique live borrow. + // SAFETY: `sleep_until` + `tick_raw` hold no `&mut PackageManager` + // across `Closure::is_done`. unsafe { PackageManager::sleep_until(mgr, &mut closure, Closure::is_done) }; if this.options.log_level.show_progress() { @@ -1108,8 +1106,7 @@ pub fn enqueue_dependency_with_main_and_success_fn( ); if let Some(new_resolve_result) = get_or_put_resolved_package_with_find_result( - // SAFETY: see `this_ptr` note above. - unsafe { &mut *this_ptr }, + this, name_hash, name, dependency, @@ -1234,8 +1231,9 @@ pub fn enqueue_dependency_with_main_and_success_fn( } if let Some(repo_fd) = this.git_repositories.get(&clone_id).copied() { + // SAFETY: sole live loader borrow; `log` is a disjoint field. let resolved = Repository::find_commit( - this.env_mut(), + unsafe { this.env_mut() }, this.log_mut(), repo_fd, alias, @@ -1729,7 +1727,8 @@ fn enqueue_git_clone( &mut crate::network_task::filename_store_appender(), ) .expect("unreachable"), - env: crate::repository::SharedEnv::get(this.env_mut()), + // SAFETY: sole live loader borrow. + env: crate::repository::SharedEnv::get(unsafe { this.env_mut() }), dep_id, res: *res, }), @@ -1815,6 +1814,7 @@ pub fn enqueue_git_checkout( &mut crate::network_task::filename_store_appender(), ) .expect("unreachable"), + // SAFETY: sole live loader borrow (outer `unsafe` block covers this call). env: crate::repository::SharedEnv::get(this.env_mut()), }), }, @@ -2007,8 +2007,7 @@ fn get_or_put_resolved_package_with_find_result( // `manager.lockfile`. this.to_update // If updating, only update packages in the current workspace - && unsafe { &*(*this_ptr).lockfile } - .is_root_dependency(unsafe { &mut *this_ptr }, dependency_id) + && unsafe { &*(*this_ptr).lockfile }.is_root_dependency(this, dependency_id) // no need to do a look up if update requests are empty (`bun update` with no args) && (this.update_requests.is_empty() || this.updating_packages.contains( @@ -2080,10 +2079,7 @@ fn get_or_put_resolved_package_with_find_result( // order-independence guard can tell them apart from range-resolved // entries (which it treats as network-order artefacts). if version.tag == dependency::version::Tag::Npm && version.npm().version.is_exact() { - // SAFETY: `this_ptr` is the sole live `&mut PackageManager` here; - // `lockfile.exact_pinned` is disjoint from `package` (returned - // by-value above). - unsafe { &mut *(*this_ptr).lockfile }.mark_exact_pin(package.meta.id); + this.lockfile.mark_exact_pin(package.meta.id); } // Use scopeguard so success_fn runs on every // return below (including the `?` paths). The guard owns the raw pointer so the @@ -2537,8 +2533,7 @@ fn get_or_put_resolved_package( let manifest_ref: bun_ptr::BackRef = bun_ptr::BackRef::new(manifest); get_or_put_resolved_package_with_find_result( - // SAFETY: see `this_ptr` note above. - unsafe { &mut *this_ptr }, + this, name_hash, name, dependency, diff --git a/src/install/PackageManager/PopulateManifestCache.rs b/src/install/PackageManager/PopulateManifestCache.rs index 78df4289355f..3511fecc6e6b 100644 --- a/src/install/PackageManager/PopulateManifestCache.rs +++ b/src/install/PackageManager/PopulateManifestCache.rs @@ -193,24 +193,17 @@ pub fn populate_manifest_cache( ManifestLoad::LoadFromMemoryFallbackToDisk, needs_extended_manifest, ); + // SAFETY: `manager_ptr` is the SRW provenance root; the calls + // below only touch the network-task pool / progress bar / log, + // never `lockfile.buffers` or `lockfile.packages`, so the + // outstanding shared slices (`pkg_name_slice`, `dep`) stay valid. + let manager = unsafe { &mut *manager_ptr }; if cached.is_none() { - start_manifest_task( - // SAFETY: `manager_ptr` is the SRW provenance root; - // `start_manifest_task` only touches the network-task - // pool / progress bar / log, never `lockfile.buffers` - // or `lockfile.packages`, so the outstanding shared - // slices (`pkg_name_slice`, `dep`) stay valid. - unsafe { &mut *manager_ptr }, - pkg_name_slice, - dep, - needs_extended_manifest, - )?; + start_manifest_task(manager, pkg_name_slice, dep, needs_extended_manifest)?; } - // SAFETY: SRW root; network-queue flush does not mutate `lockfile`. - run_tasks::flush_network_queue(unsafe { &mut *manager_ptr }); - // SAFETY: SRW root; task scheduler does not mutate `lockfile`. - let _ = run_tasks::schedule_tasks(unsafe { &mut *manager_ptr }); + run_tasks::flush_network_queue(manager); + let _ = run_tasks::schedule_tasks(manager); } } Packages::Ids(ids) => { @@ -249,30 +242,21 @@ pub fn populate_manifest_cache( needs_extended_manifest, ); if cached.is_none() { - start_manifest_task( - // SAFETY: `manager_ptr` is the SRW provenance - // root; `start_manifest_task` only touches the - // network-task pool / progress bar / log, never - // `lockfile.buffers` or `lockfile.packages`, so - // `package_name` / `dep` stay valid. - unsafe { &mut *manager_ptr }, - package_name, - dep, - needs_extended_manifest, - )?; + // SAFETY: `manager_ptr` is the SRW provenance root; the + // calls below only touch the network-task pool / + // progress bar / log, never `lockfile.buffers` or + // `lockfile.packages`, so `package_name` / `dep` stay valid. + let manager = unsafe { &mut *manager_ptr }; + start_manifest_task(manager, package_name, dep, needs_extended_manifest)?; - // SAFETY: SRW root; network-queue flush does not mutate `lockfile`. - run_tasks::flush_network_queue(unsafe { &mut *manager_ptr }); - // SAFETY: SRW root; task scheduler does not mutate `lockfile`. - let _ = run_tasks::schedule_tasks(unsafe { &mut *manager_ptr }); + run_tasks::flush_network_queue(manager); + let _ = run_tasks::schedule_tasks(manager); } } } } } - // SAFETY: provenance root; no live shared borrows of `*manager_ptr` remain. - let manager = unsafe { &mut *manager_ptr }; run_tasks::flush_network_queue(manager); let _ = run_tasks::schedule_tasks(manager); diff --git a/src/install/PackageManager/ProgressStrings.rs b/src/install/PackageManager/ProgressStrings.rs index 4d6865c1d91b..9f1722723670 100644 --- a/src/install/PackageManager/ProgressStrings.rs +++ b/src/install/PackageManager/ProgressStrings.rs @@ -105,30 +105,42 @@ impl ProgressStrings { } impl PackageManager { + /// Fill `progress_name_buf` with `emoji` + `name`; returns the byte length. + fn write_progress_name(&mut self, name: &[u8], emoji: &[u8]) -> usize { + if Output::enable_ansi_colors_stderr() { + if IS_FIRST { + self.progress_name_buf[..emoji.len()].copy_from_slice(emoji); + } + self.progress_name_buf[emoji.len()..][..name.len()].copy_from_slice(name); + emoji.len() + name.len() + } else { + self.progress_name_buf[..name.len()].copy_from_slice(name); + name.len() + } + } + + /// Name `downloads_node`. Fills the buffer and re-derives the node after, so + /// no `&mut ProgressNode` is live alongside the `&mut self` it aliases — + /// `downloads_node` points at `self.progress.root`. + pub fn set_downloads_node_name(&mut self, name: &[u8], emoji: &[u8]) { + let len = self.write_progress_name::(name, emoji); + // SAFETY: `progress_name_buf` is an inline field of the leaked singleton, + // so it outlives every node that references it. + let named: &'static [u8] = + unsafe { bun_ptr::detach_lifetime(&self.progress_name_buf[..len]) }; + self.downloads_node_mut().name = named; + } + pub fn set_node_name( &mut self, node: &mut ProgressNode, name: &[u8], emoji: &[u8], ) { - // SAFETY: `node` is `self.downloads_node` / `self.scripts_node`, both of - // which point at storage owned by (or outliving) this `PackageManager` - // singleton; `progress_name_buf` is an inline field of that same - // singleton, so the buffer outlives every node that references it and - // erasing the slice lifetime to `'static` is sound. - unsafe { - let len = if Output::enable_ansi_colors_stderr() { - if IS_FIRST { - self.progress_name_buf[..emoji.len()].copy_from_slice(emoji); - } - self.progress_name_buf[emoji.len()..][..name.len()].copy_from_slice(name); - emoji.len() + name.len() - } else { - self.progress_name_buf[..name.len()].copy_from_slice(name); - name.len() - }; - node.name = bun_ptr::detach_lifetime(&self.progress_name_buf[..len]); - } + let len = self.write_progress_name::(name, emoji); + // SAFETY: `progress_name_buf` is an inline field of the leaked singleton, + // so it outlives every node that references it. + node.name = unsafe { bun_ptr::detach_lifetime(&self.progress_name_buf[..len]) }; } pub fn start_progress_bar_if_none(&mut self) { @@ -141,19 +153,18 @@ impl PackageManager { self.progress.supports_ansi_escape_codes = Output::enable_ansi_colors_stderr(); // `Progress::start` returns `&mut Node` borrowing `self.progress`; // decay to a raw ptr immediately so the exclusive borrow ends before we - // re-borrow `&mut self` for `set_node_name` / `progress.refresh()`. + // re-borrow `&mut self`. let node: *mut ProgressNode = self.progress.start(ProgressStrings::download(), 0); self.downloads_node = Some(node); - self.set_node_name::( - self.downloads_node_mut(), + self.set_downloads_node_name::( ProgressStrings::DOWNLOAD_NO_EMOJI_.as_bytes(), ProgressStrings::DOWNLOAD_EMOJI.as_bytes(), ); - // `downloads_node` was just stashed above; route through the accessor - // (single unsafe site) instead of re-dereffing the raw `node` here. + let estimated = (self.total_tasks + self.extracted_count) as usize; + let completed = (self.total_tasks - self.pending_task_count()) as usize; let dn = self.downloads_node_mut(); - dn.set_estimated_total_items((self.total_tasks + self.extracted_count) as usize); - dn.set_completed_items((self.total_tasks - self.pending_task_count()) as usize); + dn.set_estimated_total_items(estimated); + dn.set_completed_items(completed); dn.activate(); self.progress.refresh(); } diff --git a/src/install/PackageManager/runTasks.rs b/src/install/PackageManager/runTasks.rs index aa74b85ee9ef..f358ce341375 100644 --- a/src/install/PackageManager/runTasks.rs +++ b/src/install/PackageManager/runTasks.rs @@ -176,14 +176,13 @@ pub fn run_tasks( if C::PROGRESS_BAR { let completed_items = (manager.total_tasks - manager.pending_task_count()) as usize; - // SAFETY: `downloads_node` set by `start_progress_bar_if_none`; - // points into `manager.progress` which is live. + let total_tasks = manager.total_tasks as usize; let node = manager.downloads_node_mut(); if completed_items != node.unprotected_completed_items.load(Ordering::Relaxed) || has_updated_this_run.get() { node.set_completed_items(completed_items); - node.set_estimated_total_items(manager.total_tasks as usize); + node.set_estimated_total_items(total_tasks); } } manager.downloads_node_mut().activate(); @@ -298,7 +297,9 @@ pub fn run_tasks( false, Some(InstallCtx { entry_id, - installer: installer_ptr, + // SAFETY: `installer_ptr` is the live installer owned by this + // loop; it outlives every subprocess it spawns. + installer: unsafe { bun_ptr::BackRef::from_raw(installer_ptr) }, }), ); if let Err(err) = spawn_res { @@ -359,8 +360,7 @@ pub fn run_tasks( let is_extended_manifest = *is_extended_manifest; if log_level.show_progress() { if !has_updated_this_run.get() { - manager.set_node_name::( - manager.downloads_node_mut(), + manager.set_downloads_node_name::( name, ProgressStrings::DOWNLOAD_EMOJI.as_bytes(), ); @@ -889,8 +889,7 @@ pub fn run_tasks( if log_level.show_progress() { if !has_updated_this_run.get() { - manager.set_node_name::( - manager.downloads_node_mut(), + manager.set_downloads_node_name::( extract.name.slice(), ProgressStrings::EXTRACT_EMOJI.as_bytes(), ); @@ -1024,8 +1023,7 @@ pub fn run_tasks( )?; if let Some(name) = progress_name { - manager.set_node_name::( - manager.downloads_node_mut(), + manager.set_downloads_node_name::( &name, ProgressStrings::DOWNLOAD_EMOJI.as_bytes(), ); @@ -1246,8 +1244,7 @@ pub fn run_tasks( if log_level.show_progress() { if !has_updated_this_run.get() { - manager.set_node_name::( - manager.downloads_node_mut(), + manager.set_downloads_node_name::( alias, ProgressStrings::EXTRACT_EMOJI.as_bytes(), ); @@ -1370,8 +1367,9 @@ pub fn run_tasks( let repo = git.repo.slice(string_buf); use crate::repository_real::RepositoryExt as _; + // SAFETY: sole live loader borrow; `log` is a disjoint field. let resolved = crate::repository_real::Repository::find_commit( - manager.env_mut(), + unsafe { manager.env_mut() }, manager.log_mut(), repo_fd, dep_name, @@ -1415,8 +1413,7 @@ pub fn run_tasks( if log_level.show_progress() { if !has_updated_this_run.get() { - manager.set_node_name::( - manager.downloads_node_mut(), + manager.set_downloads_node_name::( name, ProgressStrings::DOWNLOAD_EMOJI.as_bytes(), ); @@ -1547,8 +1544,7 @@ pub fn run_tasks( if log_level.show_progress() { if !has_updated_this_run.get() { - manager.set_node_name::( - manager.downloads_node_mut(), + manager.set_downloads_node_name::( alias.slice(), ProgressStrings::DOWNLOAD_EMOJI.as_bytes(), ); @@ -1947,15 +1943,12 @@ fn process_dependency_list_for_ctx( extract_ctx: &mut C::Ctx, install_peer: bool, ) -> Result<(), bun_core::Error> { - let ctx_ptr: *mut C::Ctx = extract_ctx; manager.process_dependency_list( dependency_list, (), if C::HAS_ON_RESOLVE { Some(move |()| { - // SAFETY: `ctx_ptr` derived from a unique `&mut` that outlives - // this closure; `process_dependency_list` does not alias it. - C::on_resolve(unsafe { &mut *ctx_ptr }); + C::on_resolve(extract_ctx); }) } else { None diff --git a/src/install/PackageManager/updatePackageJSONAndInstall.rs b/src/install/PackageManager/updatePackageJSONAndInstall.rs index bf38804aeb70..e109328a847c 100644 --- a/src/install/PackageManager/updatePackageJSONAndInstall.rs +++ b/src/install/PackageManager/updatePackageJSONAndInstall.rs @@ -80,8 +80,9 @@ fn update_package_json_and_install_with_manager_with_updates_and_update_requests // into `pm.known_npm_aliases` for `npm:`-aliased positionals. Some(manager), // SAFETY: `ctx.log` is set once during `Command::create()` (process- - // lifetime singleton) and is never null afterward. - unsafe { &mut *ctx.log }, + // lifetime singleton) and is never null afterward. No other `&mut Log` + // is live across this call: `parse` only reborrows this one. + unsafe { ctx.log_mut() }, positionals, update_requests, subcommand, diff --git a/src/install/auto_installer.rs b/src/install/auto_installer.rs index 2f723719b7fe..a94f8dad6a9e 100644 --- a/src/install/auto_installer.rs +++ b/src/install/auto_installer.rs @@ -200,14 +200,10 @@ impl hooks::AutoInstaller for PackageManager { // `PackageJsonView` interface so this impl does not need to name // `bun_resolver::PackageJSON` directly. - // Reshaped for borrowck — `string_builder!` borrows - // `self.lockfile` mutably while `dep.clone_in` needs `&mut self`. - // Use a raw pointer for the disjoint reborrow (same approach as - // `Package::from_package_json`). - let pm: *mut PackageManager = self; - // SAFETY: `pm` derives from `&mut self`; reborrows below are disjoint - // from `string_builder`'s borrow of `lockfile.{string_bytes,string_pool}`. - let lockfile: &mut lockfile::Lockfile = unsafe { &mut *(*pm).lockfile }; + // `string_builder!` borrows `lockfile.{string_bytes,string_pool}`, + // while `dep.clone_in` only needs the `NpmAliasRegistry` half of + // `self` — `known_npm_aliases`, a disjoint field. + let lockfile: &mut lockfile::Lockfile = &mut *self.lockfile; let mut package = Package::default(); let mut string_builder = crate::string_builder!(lockfile); @@ -251,10 +247,7 @@ impl hooks::AutoInstaller for PackageManager { if !dep.behavior.is_enabled(features) { continue; } - // SAFETY: `pm` is the unique owner; `string_builder` borrows - // disjoint lockfile fields. - let pm_ref: &mut PackageManager = unsafe { &mut *pm }; - match dep.clone_in(pm_ref, source_buf, &mut string_builder) { + match dep.clone_in(&mut self.known_npm_aliases, source_buf, &mut string_builder) { Ok(cloned) => dependencies[0] = cloned, Err(e) => { // `string_builder.clamp()` must run on the diff --git a/src/install/hoisted_install.rs b/src/install/hoisted_install.rs index 86206cbe8949..ec2bc6297d02 100644 --- a/src/install/hoisted_install.rs +++ b/src/install/hoisted_install.rs @@ -67,12 +67,10 @@ pub(crate) fn install_hoisted_packages( // Restore-buffers guard — side-effecting rollback, // not a free. Captures `*mut PackageManager` so the guard can write back - // through the same provenance root the body uses (see `mgr_ptr` below). + // through the same provenance root the body uses. + // Under Tree Borrows a raw pointer coerced from `this` carries `this`'s own + // tag, so `mgr_ptr` and `this` are interchangeable: no reborrow needed. let mgr_ptr: *mut PackageManager = this; - // SAFETY: `mgr_ptr` is freshly derived from the unique `&mut` fn param; - // shadowing `this` with a reborrow through it makes every body access a - // child of `mgr_ptr`, so the guard's later derefs keep provenance. - let this = unsafe { &mut *mgr_ptr }; let original_trees = core::mem::take(&mut this.lockfile.buffers.trees); let original_tree_dep_ids = core::mem::take(&mut this.lockfile.buffers.hoisted_dependencies); @@ -106,14 +104,6 @@ pub(crate) fn install_hoisted_packages( )?; } } - // Re-derive after `filter()` so every subsequent `this` use (progress - // setup through the install loop) is a fresh child of `mgr_ptr` under - // Stacked Borrows — `&mut *mgr_ptr` inside the block above popped the - // line-77 reborrow's tag. - // SAFETY: `mgr_ptr` is the provenance root derived from the unique `&mut` - // fn param; the line-77 reborrow's tag was popped by `&mut *mgr_ptr` in the - // block above, so no other borrow of `*mgr_ptr` is live here. - let this = unsafe { &mut *mgr_ptr }; let _restore_buffers = scopeguard::guard( (original_trees, original_tree_dep_ids), @@ -154,17 +144,15 @@ pub(crate) fn install_hoisted_packages( // `defer { progress.root.end(); progress = .{} }` let _end_progress = scopeguard::guard(log_level, move |log_level| { + // SAFETY: `mgr_ptr` provenance — see `_restore_buffers` note. + let this = unsafe { &mut *mgr_ptr }; if log_level.show_progress() { - // SAFETY: `mgr_ptr` provenance — see `_restore_buffers` note. - let this = unsafe { &mut *mgr_ptr }; this.progress.root.end(); this.progress = Progress::default(); } // Defensive: the stored progress-node pointers target stack locals in // this frame; clear them so `scripts_node_mut()` / `downloads_node_mut()` // can't observe a dangling pointer after the install pass returns. - // SAFETY: `mgr_ptr` provenance — see `_restore_buffers` note. - let this = unsafe { &mut *mgr_ptr }; this.scripts_node = None; this.downloads_node = None; }); diff --git a/src/install/isolated_install.rs b/src/install/isolated_install.rs index 2dac7d988071..a9f1f32443d4 100644 --- a/src/install/isolated_install.rs +++ b/src/install/isolated_install.rs @@ -177,7 +177,10 @@ impl<'a, 'b> Wait<'a, 'b> { // `Installer.manager` is a BACKREF raw pointer; `manager_mut()` // materializes the unique `&mut PackageManager` for this main-thread // tick without aliasing `&mut Installer`. - let pkg_manager = self.installer.manager_mut(); + // SAFETY: main thread. `run_tasks` requires both this and + // `&mut Installer`; its callbacks re-derive their own `&mut`, so this + // one must not be used from inside them. + let pkg_manager = unsafe { self.installer.manager_mut() }; let log_level = pkg_manager.options.log_level; // `run_tasks` must not call `installer.manager_mut()` — `pkg_manager` // is the live `&mut PackageManager` for this call. @@ -191,7 +194,8 @@ impl<'a, 'b> Wait<'a, 'b> { return true; } - let pkg_manager = self.installer.manager_mut(); + // SAFETY: main thread; `run_tasks` has returned, no other `&mut` live. + let pkg_manager = unsafe { self.installer.manager_mut() }; if let Some(node) = pkg_manager.scripts_node_mut() { // if we're just waiting for scripts, make it known. @@ -2014,13 +2018,15 @@ pub(crate) fn install_isolated_packages( // the local would pop the stored raw's Stacked Borrows tag, and the // run-tasks tick callback dereferences that raw via `scripts_node_mut()`. let scripts_node_ptr = manager.scripts_node; - // `Installer.manager` is a BACKREF raw pointer; copying `manager_ptr` - // does not move `manager`, so the body keeps using `manager` via the - // shadow-reborrow below. + // `Installer.manager` is a BACKREF; copying `manager_ptr` does not move + // `manager`, so the body keeps using `manager` via the shadow-reborrow below. let manager_ptr: *mut PackageManager = manager; let mut installer = store::Installer { lockfile: lockfile_ptr, - manager: manager_ptr, + // SAFETY: `manager_ptr` came from `&mut PackageManager`, so the BACKREF + // keeps write provenance for `manager_mut()`; the pointee outlives the + // `Installer` and every `Task`. + manager: unsafe { bun_ptr::ParentRef::from_raw_mut(manager_ptr) }, command_ctx, installed, install_node: if show_progress { @@ -2081,13 +2087,14 @@ pub(crate) fn install_isolated_packages( .any(|r| r.tag == ResolutionTag::Symlink) { let _ = crate::package_manager_real::directories::global_link_dir_path( - installer.manager_mut(), + // SAFETY: main thread; no other `&mut PackageManager` is live. + unsafe { installer.manager_mut() }, ); } // add the pending task count upfront - installer - .manager_mut() + // SAFETY: main thread; no other `&mut PackageManager` is live here. + unsafe { installer.manager_mut() } .increment_pending_tasks(u32::try_from(store.entries.len()).expect("int cast")); for _entry_id in 0..store.entries.len() { let entry_id = store::entry::Id::from(u32::try_from(_entry_id).expect("int cast")); @@ -2338,8 +2345,9 @@ pub(crate) fn install_isolated_packages( let mut pkg_cache_dir_subpath: AutoRelPath = AutoRelPath::from(cache_subpath_z.as_bytes()).assume_ok(); + // SAFETY: main thread; no other `&mut PackageManager` live. let (cache_dir, cache_dir_path) = - installer.manager_mut().get_cache_directory_and_abs_path(); + unsafe { installer.manager_mut() }.get_cache_directory_and_abs_path(); let _ = &cache_dir_path; // dropped at scope exit let missing_from_cache = match installer.manager().get_preinstall_state(pkg_id) @@ -2367,7 +2375,8 @@ pub(crate) fn install_isolated_packages( .unwrap_or(false), }; if exists { - installer.manager_mut().set_preinstall_state( + // SAFETY: main thread; no other `&mut` live. + unsafe { installer.manager_mut() }.set_preinstall_state( pkg_id, install::PreinstallState::Done, ); @@ -2406,7 +2415,8 @@ pub(crate) fn install_isolated_packages( match pkg_res_tag { ResolutionTag::Npm => { - match installer.manager_mut().enqueue_package_for_download( + // SAFETY: main thread; no other `&mut` live. + match unsafe { installer.manager_mut() }.enqueue_package_for_download( pkg_name.slice(string_buf), dep_id, pkg_id, @@ -2444,7 +2454,8 @@ pub(crate) fn install_isolated_packages( } } ResolutionTag::Git => { - installer.manager_mut().enqueue_git_for_checkout( + // SAFETY: main thread; no other `&mut` live. + unsafe { installer.manager_mut() }.enqueue_git_for_checkout( dep_id, dep.name.slice(string_buf), &pkg_res, @@ -2458,7 +2469,8 @@ pub(crate) fn install_isolated_packages( // (the two arms share `Repository` layout). let url = installer.manager().alloc_github_url(pkg_res.github()); // (Drop frees url) - match installer.manager_mut().enqueue_tarball_for_download( + // SAFETY: main thread; no other `&mut` live. + match unsafe { installer.manager_mut() }.enqueue_tarball_for_download( dep_id, pkg_id, &url, @@ -2493,7 +2505,8 @@ pub(crate) fn install_isolated_packages( } } ResolutionTag::LocalTarball => { - installer.manager_mut().enqueue_tarball_for_reading( + // SAFETY: main thread; no other `&mut` live. + unsafe { installer.manager_mut() }.enqueue_tarball_for_reading( dep_id, pkg_id, dep.name.slice(string_buf), @@ -2502,7 +2515,8 @@ pub(crate) fn install_isolated_packages( ); } ResolutionTag::RemoteTarball => { - match installer.manager_mut().enqueue_tarball_for_download( + // SAFETY: main thread; no other `&mut` live. + match unsafe { installer.manager_mut() }.enqueue_tarball_for_download( dep_id, pkg_id, pkg_res.remote_tarball().slice(string_buf), @@ -2575,8 +2589,10 @@ pub(crate) fn install_isolated_packages( } // Defensive: clear the stack-local progress-node pointers so the // accessors can't observe dangling pointers after this frame returns. - installer.manager_mut().scripts_node = None; - installer.manager_mut().downloads_node = None; + // SAFETY: main thread; tasks are drained, no other `&mut` live. + let manager = unsafe { installer.manager_mut() }; + manager.scripts_node = None; + manager.downloads_node = None; if Environment::CI_ASSERT { let mut done = true; diff --git a/src/install/isolated_install/Installer.rs b/src/install/isolated_install/Installer.rs index 8975198633cd..0922e942da5b 100644 --- a/src/install/isolated_install/Installer.rs +++ b/src/install/isolated_install/Installer.rs @@ -84,12 +84,10 @@ pub struct Installer<'a> { pub scripts_node: Option>, pub is_new_bun_modules: bool, - /// BACKREF. Raw pointer (not `&'a mut`) because - /// `Task::run`/`Task::callback` execute concurrently on the thread pool - /// and each derefs this field; a `&'a mut` here would assert exclusivity - /// every concurrent task violates. Never null. Access via `manager()` / - /// `manager_mut()` (main thread only for `_mut`). - pub manager: *mut PackageManager, + /// BACKREF. `ParentRef` (not `&'a mut`) because `Task::run`/`Task::callback` + /// execute concurrently on the thread pool and each derefs this field. + /// Access via `manager()` / `manager_mut()` (main thread only for `_mut`). + pub manager: bun_ptr::ParentRef, pub command_ctx: Command::Context<'a>, pub store: &'a Store, @@ -128,23 +126,26 @@ impl<'a> Installer<'a> { // BACKREF accessors — `manager` points outside `Self`; see field doc. #[inline] pub fn manager(&self) -> &'a PackageManager { - // SAFETY: BACKREF — never null; pointee outlives `'a`. - unsafe { &*self.manager } + // SAFETY: BACKREF — pointee outlives `'a`. `ParentRef::get` would tie the + // borrow to `&self`; callers need `'a`. + unsafe { &*self.manager.as_ptr() } } + /// # Safety + /// Main thread only: `Task::run` / `Task::callback` deref `self.manager` + /// on the pool, so nothing may be written through the result while pool + /// tasks are in flight. The result must be the only live + /// `&mut PackageManager` — do not re-derive one, directly or through an + /// `Installer` callback, while an outer one is still in use. `run_tasks` + /// is passed one and its callbacks re-derive: that overlap is a known + /// defect of `runTasks::run_tasks`'s signature, not licensed here. + /// `*self.manager` outlives `'a`; the return is `'a` (not elided) so + /// `start_task` can hold it across `&mut self.tasks[i]`. #[inline] #[allow(clippy::mut_from_ref)] - pub fn manager_mut(&self) -> &'a mut PackageManager { - // SAFETY: BACKREF — never null; disjoint from `*self`. Return is `'a` - // (not elided) so `start_task` can hold it across `&mut self.tasks[i]` - // — same field-disjoint shape the prior `&'a mut` field permitted. - // Caller must be on the main thread (only main mutates - // `PackageManager`; `Task::run` / `Task::callback` on the pool read - // via the raw field, never this accessor). A - // `debug_assert!(is_main_thread())` is deferred until - // `bun_crash_handler::cli_state::set_main_thread_id` is actually - // wired at startup — today the sentinel is never set, so the assert - // would fire unconditionally. - unsafe { &mut *self.manager } + pub unsafe fn manager_mut(&self) -> &'a mut PackageManager { + // SAFETY: caller upholds the contract above; the BACKREF was built with + // `from_raw_mut`, so it carries write provenance. + unsafe { self.manager.assume_mut() } } #[inline] pub fn lockfile(&self) -> &'a Lockfile { @@ -159,7 +160,8 @@ impl<'a> Installer<'a> { /// Called from main thread pub fn start_task(&mut self, entry_id: StoreEntryId) { - let manager = self.manager_mut(); + // SAFETY: main thread; no other `&mut PackageManager` is live here. + let manager = unsafe { self.manager_mut() }; let task = &mut self.tasks[entry_id.get() as usize]; debug_assert!(matches!( task.result, @@ -178,7 +180,10 @@ impl<'a> Installer<'a> { } pub fn on_package_extracted(&mut self, task_id: crate::package_manager_task::Id) { - if let Some(removed) = self.manager_mut().task_queue.remove(&task_id) { + // SAFETY: main thread. Bound in a `let` so the `&mut` ends at the + // semicolon, before `start_task` below re-derives one. + let removed = unsafe { self.manager_mut() }.task_queue.remove(&task_id); + if let Some(removed) = removed { let store = self.store; let node_pkg_ids = store.nodes.items_pkg_id(); @@ -238,7 +243,10 @@ impl<'a> Installer<'a> { err: bun_core::Error, url: &[u8], ) { - if let Some(removed) = self.manager_mut().task_queue.remove(&task_id) { + // SAFETY: main thread. Bound in a `let` so the `&mut` ends at the + // semicolon, before `on_task_fail` below re-derives one. + let removed = unsafe { self.manager_mut() }.task_queue.remove(&task_id); + if let Some(removed) = removed { let callbacks = removed; let entry_steps = self.store.entries.items_step(); @@ -286,23 +294,16 @@ impl<'a> Installer<'a> { let node_pkg_ids = store.nodes.items_pkg_id(); let pkg_id = node_pkg_ids[node_id.get() as usize]; let patch_task_ptr = install::PatchTask::new_apply_patch_hash( - self.manager_mut(), + // SAFETY: main thread; no other `&mut PackageManager` is live here. + unsafe { self.manager_mut() }, pkg_id, patch.contents_hash, patch.name_and_version_hash, ); - // SAFETY: `new_apply_patch_hash` returns a freshly Box-allocated PatchTask; - // sole ownership lives in this scope. - struct PatchTaskGuard(*mut install::PatchTask); - impl Drop for PatchTaskGuard { - fn drop(&mut self) { - // SAFETY: exclusive owner; created by `heap::alloc` in `new_*`. - unsafe { install::PatchTask::destroy(self.0) }; - } - } - let _guard = PatchTaskGuard(patch_task_ptr); - // SAFETY: exclusive owner — see above. - let patch_task = unsafe { &mut *patch_task_ptr }; + // SAFETY: `new_apply_patch_hash` returns a freshly `Box`-allocated PatchTask + // via `heap::into_raw`; reclaim that same `Box` so ownership is plain and an + // unwind (or the early return below) frees it without a guard. + let mut patch_task = unsafe { bun_core::heap::take(patch_task_ptr) }; // Every peer variant shares one patched cache dir (named by the patch // contents hash, not the peer set). Once it exists, reuse it: rebuilding // it replaces the directory under earlier entries' running hardlink tasks. @@ -320,6 +321,7 @@ impl<'a> Installer<'a> { apply.logger.clone_to_with_recycled(log, true); } } + patch_task.destroy(); } /// Called from main thread @@ -447,7 +449,8 @@ impl<'a> Installer<'a> { } pub fn decrement_pending_tasks(&mut self) { - self.manager_mut().decrement_pending_tasks(); + // SAFETY: main thread; no other `&mut PackageManager` is live here. + unsafe { self.manager_mut() }.decrement_pending_tasks(); } /// Called from main thread @@ -835,18 +838,12 @@ impl Task { // `&Installer` and alias `*manager_ptr` / `*lockfile_ptr`. let installer_ptr = self.installer; let installer = installer_ptr.get(); - let manager_ptr: *mut PackageManager = installer.manager; + // BACKREF copy — read-only deref sites below go through safe `Deref`/`get()`. + // Mutation and narrowed `addr_of_mut!` field projections still go through the + // raw `manager_ptr`, which shares `manager_ref`'s provenance tag. + let manager_ref = installer.manager; + let manager_ptr: *mut PackageManager = manager_ref.as_mut_ptr(); let lockfile_ptr: *mut Lockfile = installer.lockfile; - // BACKREF — `manager_ptr` is non-null and the `PackageManager` outlives - // every `Task` (see top-of-fn note). Wrapped once as `ParentRef` so the - // read-only deref sites below go through safe `Deref`/`get()` instead - // of per-site `unsafe { &* }`. Mutation and narrowed `addr_of_mut!` - // field projections still go through the raw `manager_ptr` directly - // (same provenance tag as `manager_ref.ptr`). Safe `From` - // construction — non-null is guaranteed by the BACKREF field invariant. - let manager_ref = bun_ptr::ParentRef::::from( - core::ptr::NonNull::new(manager_ptr).expect("Installer.manager BACKREF is non-null"), - ); // Read-only `&Lockfile` via the BACKREF accessor (centralised deref); // same provenance as `&*lockfile_ptr`. `lockfile_ptr` itself is kept // raw for the narrowed `addr_of_mut!((*lockfile_ptr).trusted_dependencies)` @@ -1130,8 +1127,10 @@ impl Task { // Concurrent task threads may race the same once-init path — that // is a data-level race the once-init guards, not an aliasing // violation here because no long-lived `&mut PackageManager` exists. - let (cache_dir, cache_dir_path) = - directories::get_cache_directory_and_abs_path(unsafe { &mut *manager_ptr }); + let (cache_dir, cache_dir_path) = directories::get_cache_directory_and_abs_path( + // SAFETY: see above — no other borrow of the parent is live. + unsafe { manager_ref.assume_mut() }, + ); let uses_global_store = installer.entry_uses_global_store(self.entry_id); @@ -1948,10 +1947,17 @@ impl Task { } } - /// Called from task thread + /// Called from task thread. ABI-fixed pool trampoline: one deref, then a + /// safe `&mut self` method. pub unsafe fn callback(task: *mut thread_pool::Task) { - // SAFETY: task points to Task.task field - let this: &mut Task = unsafe { &mut *bun_core::from_field_ptr!(Task, task, task) }; + // SAFETY: `task` points to the `task` field of a live `Task`; the pool + // grants exclusive access for the duration of this call. + unsafe { &mut *bun_core::from_field_ptr!(Task, task, task) }.run_on_pool(); + } + + /// Called from task thread + fn run_on_pool(&mut self) { + let this: &mut Task = self; let res = match this.run() { Ok(r) => r, @@ -1969,7 +1975,7 @@ impl Task { // would not prevent the `&mut` lifetimes from overlapping). let installer_ptr = this.installer; let installer = installer_ptr.get(); - let manager_ptr: *mut PackageManager = installer.manager; + let manager_ptr: *mut PackageManager = installer.manager.as_mut_ptr(); match res { Yield::Yield => {} diff --git a/src/install/lifecycle_script_runner.rs b/src/install/lifecycle_script_runner.rs index 50f40b33fea6..5254283a016a 100644 --- a/src/install/lifecycle_script_runner.rs +++ b/src/install/lifecycle_script_runner.rs @@ -289,26 +289,10 @@ pub struct LifecycleScriptSubprocess<'a> { pub struct InstallCtx<'a> { pub entry_id: entry::Id, - /// Raw `*mut` for the same reason as - /// `LifecycleScriptSubprocess::manager` — `on_task_complete`/`start_task` + /// Back-reference to the `Installer` that spawned the script; it outlives + /// every in-flight `LifecycleScriptSubprocess`. `on_task_complete`/`start_task` /// mutate Installer state from inside an exit-handler callback. - pub installer: *mut Installer<'a>, -} - -impl<'a> InstallCtx<'a> { - /// BACKREF accessor — single `unsafe` deref for the set-once `installer` - /// pointer so call sites in `on_process_exit` are safe. - /// - /// SAFETY (encapsulated): `installer` is non-null and outlives every - /// `LifecycleScriptSubprocess` (the `Installer` owns the script-spawn - /// loop). Exit-handler callbacks run single-threaded on the main install - /// loop, so no other `&`/`&mut Installer` overlaps the returned borrow. - #[inline] - #[allow(clippy::mut_from_ref)] - fn installer_mut(&self) -> &mut Installer<'a> { - // SAFETY: see fn doc. - unsafe { &mut *self.installer } - } + pub installer: bun_ptr::BackRef>, } // `io_heap::Intrusive` takes the comparator via `HeapContext::less` on the @@ -876,7 +860,10 @@ impl<'a> LifecycleScriptSubprocess<'a> { if exit.code > 0 { if self.optional { if let Some(ctx) = &self.ctx { - let installer = ctx.installer_mut(); + let mut installer_ref = ctx.installer; + // SAFETY: exit handlers run single-threaded on the install loop; + // no other borrow of the installer is live here. + let installer = unsafe { installer_ref.get_mut() }; installer.store.entries.items_step()[ctx.entry_id.get() as usize] .store(Step::Done as u32, Ordering::Release); installer.on_task_complete(ctx.entry_id, CompleteState::Skipped); @@ -934,7 +921,10 @@ impl<'a> LifecycleScriptSubprocess<'a> { match self.current_script_index { // preinstall 0 => { - let installer = ctx.installer_mut(); + let mut installer_ref = ctx.installer; + // SAFETY: exit handlers run single-threaded on the install loop; + // no other borrow of the installer is live here. + let installer = unsafe { installer_ref.get_mut() }; let previous_step = installer.store.entries.items_step() [ctx.entry_id.get() as usize] .swap(Step::Binaries as u32, Ordering::Release); @@ -985,7 +975,10 @@ impl<'a> LifecycleScriptSubprocess<'a> { } if let Some(ctx) = &self.ctx { - let installer = ctx.installer_mut(); + let mut installer_ref = ctx.installer; + // SAFETY: exit handlers run single-threaded on the install loop; + // no other borrow of the installer is live here. + let installer = unsafe { installer_ref.get_mut() }; let previous_step = installer.store.entries.items_step() [ctx.entry_id.get() as usize] .swap(Step::Done as u32, Ordering::Release); @@ -1027,7 +1020,10 @@ impl<'a> LifecycleScriptSubprocess<'a> { Status::Err(err) => { if self.optional { if let Some(ctx) = &self.ctx { - let installer = ctx.installer_mut(); + let mut installer_ref = ctx.installer; + // SAFETY: exit handlers run single-threaded on the install loop; + // no other borrow of the installer is live here. + let installer = unsafe { installer_ref.get_mut() }; installer.store.entries.items_step()[ctx.entry_id.get() as usize] .store(Step::Done as u32, Ordering::Release); installer.on_task_complete(ctx.entry_id, CompleteState::Skipped); diff --git a/src/install/patch_install.rs b/src/install/patch_install.rs index b96ccb273148..94d9e1ce23df 100644 --- a/src/install/patch_install.rs +++ b/src/install/patch_install.rs @@ -140,20 +140,13 @@ pub struct InstallContext { impl PatchTask { /// Destroy a heap-allocated `PatchTask` previously created by - /// `new_calc_patch_hash` / `new_apply_patch_hash`. - /// - /// The owned fields (`Box<[u8]>`, `Vec`, `Log`, `Option<...>`) drop automatically, so no - /// `impl Drop` body is needed. Because `PatchTask` is held via raw pointer through the - /// intrusive `next`/thread-pool queue, the named reclaim point is `unsafe fn destroy`. - /// - /// # Safety - /// `this` must have been produced by `heap::alloc` in the `new_*` constructors below and - /// ownership must be returned here exactly once. - pub unsafe fn destroy(this: *mut Self) { - // TODO: how to deinit `this.callback.calc_hash.network_task` - // SAFETY: caller contract — `this` was produced by `heap::into_raw` in - // `new_calc_patch_hash`/`new_apply_patch_hash` and is reclaimed exactly once. - drop(unsafe { bun_core::heap::take(this) }); + /// `new_calc_patch_hash` / `new_apply_patch_hash`. The `Box` receiver is the named + /// reclaim point; every owned field drops when it goes out of scope. + // `boxed_local`: the `Box` is the point — it is the ownership unit the thread + // pool handed back, and this is where it is reclaimed. + #[allow(clippy::boxed_local)] + pub fn destroy(self: Box) { + // TODO: how to deinit `self.callback.calc_hash.network_task` } /// # Safety diff --git a/src/install/resolvers/folder_resolver.rs b/src/install/resolvers/folder_resolver.rs index d46bba14f0ac..8222a14eb7ba 100644 --- a/src/install/resolvers/folder_resolver.rs +++ b/src/install/resolvers/folder_resolver.rs @@ -50,34 +50,33 @@ impl<'a> fmt::Display for PackageWorkspaceSearchPathFormatter<'a> { )) .unwrap_or(workspace); - // SAFETY: joined[2..] is exactly MAX_PATH_BYTES bytes long. - let joined_path: &mut PathBuffer = - unsafe { &mut *joined.as_mut_ptr().add(2).cast::() }; - let mut paths = normalize_package_json_path( + // `joined[2..]` is exactly MAX_PATH_BYTES bytes long. + let Paths { rel, .. } = normalize_package_json_path( GlobalOrRelative::Relative(dependency::version::Tag::Workspace), - joined_path, + &mut joined[2..], self.manager.lockfile.str(str_to_use), ); - if !strings::starts_with_char(paths.rel, b'.') && !strings::starts_with_char(paths.rel, SEP) - { - joined[0] = b'.'; - joined[1] = SEP; - // `paths.rel` points into `joined[2..]`; extend the view backward - // by the two bytes just written via safe slicing of `joined`. - let n = paths.rel.len() + 2; - paths.rel = &joined[..n]; - } + let rel: &[u8] = + if !strings::starts_with_char(rel, b'.') && !strings::starts_with_char(rel, SEP) { + joined[0] = b'.'; + joined[1] = SEP; + // `normalize_package_json_path` wrote the same bytes at the front + // of `joined[2..]`; extend the view backward over the two written. + &joined[..rel.len() + 2] + } else { + rel + }; if self.quoted { - let quoted = QuotedFormatter { text: paths.rel }; + let quoted = QuotedFormatter { text: rel }; fmt::Display::fmt("ed, f) } else { // `fmt::Formatter` only accepts `&str`, so non-UTF-8 path bytes are emitted lossily // (U+FFFD) via `bstr::BStr`'s Display. Both current callers pass // `quoted = true`, so this branch is unreached today; if a future // caller needs byte-exact output it must use an `io::Write` sink. - write!(f, "{}", bstr::BStr::new(paths.rel)) + write!(f, "{}", bstr::BStr::new(rel)) } } } @@ -177,16 +176,18 @@ impl FolderResolverImpl for CacheFolderResolver { struct Paths<'a> { abs: &'a ZStr, - rel: &'a [u8], + /// `FileSystem::relative` returns the threadlocal relative buffer, which is + /// a separate allocation from `joined`. + rel: &'static [u8], } fn normalize_package_json_path<'a>( global_or_relative: GlobalOrRelative<'_>, - joined: &'a mut PathBuffer, + joined: &'a mut [u8], non_normalized_path: &[u8], ) -> Paths<'a> { let abs: &[u8]; - let rel: &[u8]; + let rel: &'static [u8]; // We consider it valid if there is a package.json in the folder let normalized: &[u8] = if non_normalized_path.len() == 1 && non_normalized_path[0] == b'.' { non_normalized_path @@ -291,11 +292,7 @@ fn read_package_json_from_disk( let _tracer = bun_perf::trace(bun_perf::PerfEvent::FolderResolverReadPackageJSONFromDiskWorkspace); - // SAFETY: `manager_ptr` was just derived from the live `&mut PackageManager` - // argument; `log` points into a separate `Log` allocation (see the - // borrow-splitting comment above), so this `&mut` reborrow aliases no - // other live reference. - let json = unsafe { &mut *manager_ptr } + let json = manager .workspace_package_json_cache .get_with_path(log, abs.as_bytes(), Default::default()) .unwrap()?; diff --git a/src/install_jsc/ini_jsc.rs b/src/install_jsc/ini_jsc.rs index 82da9a1479ba..b55a2fda7181 100644 --- a/src/install_jsc/ini_jsc.rs +++ b/src/install_jsc/ini_jsc.rs @@ -38,16 +38,22 @@ impl IniTestingAPIs { let mut log = Log::init(); let envjs = frame.argument(1); - // The loader is either VM-owned or built locally. Per PORTING.md §Forbidden - // (`Box::leak` is banned), keep both `Map` and `Loader` owned in fn-scope - // `Option`s and hand out a raw `*mut Loader` uniformly. Both drop at fn - // return. - let mut map_storage: Option>; - let mut env_storage: Option>; - let env: *mut dotenv::Loader<'static> = if envjs.is_empty_or_undefined_or_null() { - // SAFETY: `bun_vm()` is non-null on a constructed `JSGlobalObject`; - // `transpiler.env` is set during VM init (transpiler.rs). - global.bun_vm().as_mut().transpiler.env + let mut install = Box::new(BunInstall::default()); + let mut configs: Vec = Vec::new(); + // `Loader<'a>` is invariant in `'a`, so the VM-owned loader and a locally built + // one have unrelated types. `load_npmrc` takes `&mut Loader<'_>`, whose borrow + // lifetime is independent, so each arm calls it rather than unifying on a ptr. + let failed = if envjs.is_empty_or_undefined_or_null() { + let env = global.bun_vm().as_mut().transpiler.env_mut(); + load_npmrc( + &mut install, + env, + ZStr::from_static(b".npmrc\0"), + &mut log, + &source, + &mut configs, + ) + .is_err() } else { let mut envmap = dotenv::map::HashTable::new(); let Some(envobj) = envjs.get_object() else { @@ -82,32 +88,20 @@ impl IniTestingAPIs { )?; } - map_storage = Some(Box::new(dotenv::Map { map: envmap })); - // SAFETY-NOTE: `Loader` borrows from `map_storage`; both live until fn - // return. - let map_ref: &mut dotenv::Map = map_storage.as_deref_mut().unwrap(); - env_storage = Some(dotenv::Loader::init(map_ref)); - // `Loader<'a>` is invariant in `'a` (holds `&'a mut Map`); erase to `'static` - // via raw-pointer `.cast()` so both `if` arms unify on a single pointer type. - // The borrow does not escape this function — `load_npmrc` only reads through - // it and both `env_storage` / `map_storage` drop at fn return. - std::ptr::from_mut(env_storage.as_mut().unwrap()).cast::>() + let mut map = dotenv::Map { map: envmap }; + let mut env = dotenv::Loader::init(&mut map); + load_npmrc( + &mut install, + &mut env, + ZStr::from_static(b".npmrc\0"), + &mut log, + &source, + &mut configs, + ) + .is_err() }; - let mut install = Box::new(BunInstall::default()); - let mut configs: Vec = Vec::new(); - if load_npmrc( - &mut install, - // SAFETY: `env` points to either the VM-singleton Loader or `env_storage`; - // both outlive this call and are not aliased for its duration. - unsafe { &mut *env }, - ZStr::from_static(b".npmrc\0"), - &mut log, - &source, - &mut configs, - ) - .is_err() - { + if failed { return bun_ast_jsc::log_to_js(&log, global, b"error"); } diff --git a/src/io/ParentDeathWatchdog.rs b/src/io/ParentDeathWatchdog.rs index 663d34e6e9a0..be0a323daf9f 100644 --- a/src/io/ParentDeathWatchdog.rs +++ b/src/io/ParentDeathWatchdog.rs @@ -322,13 +322,11 @@ pub fn install_on_event_loop(handle: EventLoopCtx) { Default::default(), Owner::new(poll_tag::PARENT_DEATH_WATCHDOG, instance_ptr.cast()), ); - // SAFETY: `poll` was just allocated by `FilePoll::init`; sole `&mut` - // borrow; `register` does not re-derive the loop. - match unsafe { &mut *poll }.register( - handle.loop_mut(), - crate::file_poll::Pollable::Process, - true, - ) { + // SAFETY: on the loop's thread; `register` does not re-derive the loop, + // so no other `&mut Loop` is live across this borrow. + let loop_ = unsafe { handle.loop_mut() }; + // SAFETY: `poll` was just allocated by `FilePoll::init`; sole `&mut` borrow. + match unsafe { &mut *poll }.register(loop_, crate::file_poll::Pollable::Process, true) { bun_sys::Result::Ok(()) => { // Do not keep the event loop alive on this poll's behalf — the // watchdog must never prevent Bun from exiting when there is no @@ -349,7 +347,7 @@ pub fn install_on_event_loop(handle: EventLoopCtx) { /// `FilePoll.Owner` dispatch target — invoked from the event loop's /// `ParentDeathWatchdog` poll arm. The kqueue `NOTE_EXIT` for our parent /// fired. -pub fn on_parent_exit(_this: &mut ParentDeathWatchdog) { +pub fn on_parent_exit(_this: &ParentDeathWatchdog) { // Global.exit → Bun__onExit → on_process_exit → kill_descendants. bun_core::exit(EXIT_CODE as u32); } diff --git a/src/io/PipeReader.rs b/src/io/PipeReader.rs index 9fe49cbacee5..74b78fa2ad29 100644 --- a/src/io/PipeReader.rs +++ b/src/io/PipeReader.rs @@ -1582,51 +1582,61 @@ impl WindowsBufferedReader { if !this.flags.contains(WindowsFlags::IS_PAUSED) { // Re-snapshot — `on_read` may have mutated `this.source`. let this_ptr = core::ptr::from_mut(this).cast::(); - let file_raw: *mut crate::source::File = match this.source.as_mut() { - Some(Source::File(f)) => f.as_mut() as *mut _, - _ => core::ptr::null_mut(), - }; - if !file_raw.is_null() { - // SAFETY: see above; raw-ptr break for self-aliasing. - let file = unsafe { &mut *file_raw }; - // Can only start if file is in deinitialized state - if file.can_start() { - file.fs.data = this_ptr; - file.prepare(); - let buf = this.get_read_buffer_with_stable_memory_address(64 * 1024); - file.iov = uv::uv_buf_t::init(buf); - this.flags.insert(WindowsFlags::HAS_INFLIGHT_READ); - - let offset = if this.flags.contains(WindowsFlags::USE_PREAD) { - i64::try_from(this._offset).expect("int cast") + // Can only start if file is in deinitialized state + let can_start = match this.source.as_mut() { + Some(Source::File(file)) => { + if file.can_start() { + file.fs.data = this_ptr; + file.prepare(); + true } else { - -1 - }; - // SAFETY: `file` is fully initialized; libuv stores - // the cb and fires it on the event loop. - if let Some(err) = unsafe { - uv::uv_fs_read( - this.vtable.loop_().cast(), - &mut file.fs, - file.file, - &file.iov, - 1, - offset, - Some(Self::on_file_read), - ) - } - // Tagged `.write` even though the syscall is - // `uv_fs_read`, so user-visible `error.syscall` - // stays bit-identical with previous releases. - .to_error(sys::Tag::write) - { - file.complete(false); - this.flags.remove(WindowsFlags::HAS_INFLIGHT_READ); - this.flags.insert(WindowsFlags::IS_PAUSED); - // we should inform the error if we are unable to keep reading - this.on_read(sys::Result::Err(err), &mut [], ReadState::Progress); + false } } + _ => false, + }; + if can_start { + // The `&mut File` borrow above has ended, so the + // `&mut self` methods below need no raw-pointer break. + let iov = uv::uv_buf_t::init( + this.get_read_buffer_with_stable_memory_address(64 * 1024), + ); + this.flags.insert(WindowsFlags::HAS_INFLIGHT_READ); + + let offset = if this.flags.contains(WindowsFlags::USE_PREAD) { + i64::try_from(this._offset).expect("int cast") + } else { + -1 + }; + let loop_ = this.vtable.loop_(); + let Some(Source::File(file)) = this.source.as_mut() else { + unreachable!() + }; + file.iov = iov; + // SAFETY: `file` is fully initialized; libuv stores + // the cb and fires it on the event loop. + if let Some(err) = unsafe { + uv::uv_fs_read( + loop_.cast(), + &mut file.fs, + file.file, + &file.iov, + 1, + offset, + Some(Self::on_file_read), + ) + } + // Tagged `.write` even though the syscall is + // `uv_fs_read`, so user-visible `error.syscall` + // stays bit-identical with previous releases. + .to_error(sys::Tag::write) + { + file.complete(false); + this.flags.remove(WindowsFlags::HAS_INFLIGHT_READ); + this.flags.insert(WindowsFlags::IS_PAUSED); + // we should inform the error if we are unable to keep reading + this.on_read(sys::Result::Err(err), &mut [], ReadState::Progress); + } } } } @@ -1641,10 +1651,6 @@ impl WindowsBufferedReader { return sys::Result::Ok(()); } self.flags.remove(WindowsFlags::IS_PAUSED); - // BORROW_PARAM (raw-ptr break): the body needs `&mut self` (for - // `get_read_buffer_…`/`flags`) while also holding `&mut File` borrowed - // out of `self.source`. The boxed `File` is its own heap allocation, so - // a `*mut File` snapshot is provenance-disjoint from `&mut self`. let self_ptr = self as *mut Self as *mut c_void; let Some(source) = self.source.as_mut() else { return sys::Result::Err(sys::Error::from_code(sys::E::BADF, sys::Tag::read)); @@ -1653,10 +1659,6 @@ impl WindowsBufferedReader { match source { Source::File(file) => { - let file_raw: *mut crate::source::File = file.as_mut(); - // SAFETY: `file_raw` points into the boxed File owned by - // `self.source`; live until `self.source` is replaced. - let file = unsafe { &mut *file_raw }; // If already reading, just set data and unpause file.fs.data = self_ptr; if !file.can_start() { @@ -1665,37 +1667,6 @@ impl WindowsBufferedReader { // Start new read - set data before prepare file.prepare(); - let buf = self.get_read_buffer_with_stable_memory_address(64 * 1024); - file.iov = uv::uv_buf_t::init(buf); - self.flags.insert(WindowsFlags::HAS_INFLIGHT_READ); - - let offset = if self.flags.contains(WindowsFlags::USE_PREAD) { - i64::try_from(self._offset).expect("int cast") - } else { - -1 - }; - // SAFETY: file is fully initialized; libuv stores cb and fires - // it on the event loop. - if let Some(err) = unsafe { - uv::uv_fs_read( - self.vtable.loop_().cast(), - &mut file.fs, - file.file, - &file.iov, - 1, - offset, - Some(Self::on_file_read), - ) - } - // Tagged `.write` even though the syscall is `uv_fs_read`, so - // user-visible `error.syscall` stays bit-identical with - // previous releases. - .to_error(sys::Tag::write) - { - file.complete(false); - self.flags.remove(WindowsFlags::HAS_INFLIGHT_READ); - return sys::Result::Err(err); - } } _ => { // SAFETY: source is a live Pipe/Tty stream handle. @@ -1716,9 +1687,49 @@ impl WindowsBufferedReader { ); return sys::Result::Err(err); } + return sys::Result::Ok(()); } } + // The `&mut File` borrow ends with the match, so the `&mut self` + // methods below need no raw-pointer break; nothing above replaces + // `self.source`. + let iov = uv::uv_buf_t::init(self.get_read_buffer_with_stable_memory_address(64 * 1024)); + self.flags.insert(WindowsFlags::HAS_INFLIGHT_READ); + + let offset = if self.flags.contains(WindowsFlags::USE_PREAD) { + i64::try_from(self._offset).expect("int cast") + } else { + -1 + }; + let loop_ = self.vtable.loop_(); + let Some(Source::File(file)) = self.source.as_mut() else { + unreachable!() + }; + file.iov = iov; + // SAFETY: file is fully initialized; libuv stores cb and fires + // it on the event loop. + if let Some(err) = unsafe { + uv::uv_fs_read( + loop_.cast(), + &mut file.fs, + file.file, + &file.iov, + 1, + offset, + Some(Self::on_file_read), + ) + } + // Tagged `.write` even though the syscall is `uv_fs_read`, so + // user-visible `error.syscall` stays bit-identical with + // previous releases. + .to_error(sys::Tag::write) + { + file.complete(false); + self.flags.remove(WindowsFlags::HAS_INFLIGHT_READ); + return sys::Result::Err(err); + } + sys::Result::Ok(()) } diff --git a/src/io/lib.rs b/src/io/lib.rs index b0d1553e56b7..1fda6b0bbb40 100644 --- a/src/io/lib.rs +++ b/src/io/lib.rs @@ -65,7 +65,7 @@ pub mod parent_death_watchdog { #[inline] pub fn install_on_event_loop(_handle: EventLoopCtx) {} #[inline] - pub fn on_parent_exit(_this: &mut ParentDeathWatchdog) { + pub fn on_parent_exit(_this: &ParentDeathWatchdog) { debug_assert!(false, "ParentDeathWatchdog poll on Windows"); } } @@ -110,14 +110,14 @@ pub type OpaqueCallback = unsafe extern "C" fn(*mut core::ffi::c_void); // macro emits (and the impl-macro reads back) actually resolve from impl // crates. `Store`/`FilePoll` here are the *platform* re-exports above. // -// `platform_event_loop_ptr` is typed `*mut bun_uws_sys::Loop` (the uws +// `platform_event_loop_ptr` is typed `ParentRef` (the uws // wrapper — `PosixLoop`/`WindowsLoop`), NOT the cfg-aliased `crate::Loop` // re-export. On POSIX those coincide, but on Windows `crate::Loop` is the raw // `uv_loop_t` whereas the impl bodies // (`VirtualMachine::uws_loop` / `MiniEventLoop::loop_ptr`) hand back the wrapper. bun_dispatch::link_interface! { pub EventLoopCtx[Js, Mini] { - fn platform_event_loop_ptr() -> *mut bun_uws_sys::Loop; + fn platform_event_loop_ptr() -> bun_ptr::ParentRef; fn file_polls_ptr() -> *mut Store; // `alloc_file_poll() -> *mut FilePoll` was removed — it // returned an *uninitialized* hive slot, and any caller forming @@ -141,12 +141,15 @@ pub type EventLoopKind = EventLoopCtxKind; impl EventLoopCtx { /// SAFETY: caller must not hold another live `&mut` to the same loop /// across this borrow (resolver-style accessor; the loop is per-thread). + // `mut_from_ref` is precisely the hazard, and precisely why this fn is + // `unsafe`: `EventLoopCtx` is `Copy`, so `&mut self` could never enforce + // exclusivity over a C-owned per-thread singleton. + #[allow(clippy::mut_from_ref)] #[inline] - pub unsafe fn platform_event_loop(&self) -> &'static mut bun_uws_sys::Loop { - // Route through the single nonnull-asref accessor below; the `unsafe` - // on this fn's signature is the caller-side aliasing contract — the - // body itself needs no extra `unsafe`. - self.loop_mut() + pub unsafe fn platform_event_loop(&self) -> &mut bun_uws_sys::Loop { + // Route through the single nonnull-asref accessor below. + // SAFETY: forwarded to this fn's own caller-side aliasing contract. + unsafe { self.loop_mut() } } /// SAFETY: same aliasing hazard as [`platform_event_loop`]. #[inline] @@ -163,21 +166,21 @@ impl EventLoopCtx { // identical `ctx.platform_event_loop().op()` call sites into the single // deref inside [`loop_mut`]. // - // `loop_mut` is the single nonnull-asref accessor: `pub(crate)`, - // `&self → &mut` (so it must NOT be called twice with overlapping live - // results). Every in-crate caller is a leaf op — counter bump, - // `FilePoll::activate`/`deactivate`, `unregister` — that consumes the - // borrow before returning and never re-enters `EventLoopCtx`, so no two - // `&mut Loop` ever coexist. Widened from impl-private to crate-private so - // `posix_event_loop`/`windows_event_loop` route their N identical - // `ctx.platform_event_loop()` derefs through this single accessor. + /// The single deref of the uws loop back-pointer. + /// + /// # Safety + /// `EventLoopCtx` is `Copy`, so `&mut self` cannot enforce exclusivity + /// over the loop; the `us_loop_t` is a C-owned per-thread singleton. + /// Caller must be on the loop's thread and must not hold another live + /// `&mut Loop` (including one minted by `loop_ref` and friends) across + /// the returned borrow. + // See `platform_event_loop`: the `&self -> &mut` is the documented contract. + #[allow(clippy::mut_from_ref)] #[inline] - pub(crate) fn loop_mut(&self) -> &'static mut bun_uws_sys::Loop { - // SAFETY: per-thread set-once pointer (the uws loop singleton); the - // event loop is single-threaded so no concurrent `&mut` exists, and - // every crate-internal caller is a leaf op that drops the borrow - // before returning — see block comment above. - unsafe { &mut *self.platform_event_loop_ptr() } + pub(crate) unsafe fn loop_mut(&self) -> &mut bun_uws_sys::Loop { + // SAFETY: the back-pointer is a set-once per-thread `ParentRef`, non-null + // once the loop exists; exclusivity is the caller's obligation (above). + unsafe { self.platform_event_loop_ptr().assume_mut() } } /// Single backref-deref accessor for the per-thread `Store`. Same contract /// as [`loop_mut`]: `pub(crate)`, `&self → &mut`, must NOT be called while @@ -213,27 +216,34 @@ impl EventLoopCtx { } #[inline] pub fn loop_ref(&self) { - self.loop_mut().ref_(); + // SAFETY: leaf counter op; the borrow dies at the end of this + // statement, so it cannot overlap another `&mut Loop`. + unsafe { self.loop_mut() }.ref_(); } #[inline] pub fn loop_unref(&self) { - self.loop_mut().unref(); + // SAFETY: leaf counter op, as in `loop_ref`. + unsafe { self.loop_mut() }.unref(); } #[inline] pub fn loop_inc(&self) { - self.loop_mut().inc(); + // SAFETY: leaf counter op, as in `loop_ref`. + unsafe { self.loop_mut() }.inc(); } #[inline] pub fn loop_dec(&self) { - self.loop_mut().dec(); + // SAFETY: leaf counter op, as in `loop_ref`. + unsafe { self.loop_mut() }.dec(); } #[inline] pub fn loop_add_active(&self, n: u32) { - self.loop_mut().add_active(n); + // SAFETY: leaf counter op, as in `loop_ref`. + unsafe { self.loop_mut() }.add_active(n); } #[inline] pub fn loop_sub_active(&self, n: u32) { - self.loop_mut().sub_active(n); + // SAFETY: leaf counter op, as in `loop_ref`. + unsafe { self.loop_mut() }.sub_active(n); } #[cfg(not(windows))] #[inline] @@ -247,13 +257,13 @@ impl EventLoopCtx { } #[inline] pub fn loop_(&self) -> *mut bun_uws_sys::Loop { - self.platform_event_loop_ptr() + self.platform_event_loop_ptr().as_mut_ptr() } /// Platform-native loop pointer (`us_loop_t*` / `uv_loop_t*`); see /// [`uws_to_native`]. #[inline] pub fn native_loop(&self) -> *mut Loop { - uws_to_native(self.platform_event_loop_ptr()) + uws_to_native(self.platform_event_loop_ptr().as_mut_ptr()) } #[inline] pub fn init(h: EventLoopCtx) -> EventLoopCtx { diff --git a/src/io/posix_event_loop.rs b/src/io/posix_event_loop.rs index 8f36b301a9a7..098944c664c0 100644 --- a/src/io/posix_event_loop.rs +++ b/src/io/posix_event_loop.rs @@ -422,10 +422,10 @@ impl FilePoll { } fn deinit_possibly_defer(&mut self, vm: EventLoopCtx, force_unregister: bool) { - // `loop_mut()` is the crate-private nonnull-asref accessor (single - // deref in `EventLoopCtx`); the `&mut Loop` is consumed by `unregister` - // and dropped before any `&mut Store` is materialised. - let _ = self.unregister(vm.loop_mut(), force_unregister); + // SAFETY: the `&mut Loop` is consumed by `unregister` and dropped + // before any `&mut Store` is materialised; no other `&mut Loop` is + // live and we are on the loop's thread. + let _ = self.unregister(unsafe { vm.loop_mut() }, force_unregister); self.owner.clear(); let was_ever_registered = self.flags.contains(Flags::WasEverRegistered); @@ -621,9 +621,9 @@ impl FilePoll { pub fn on_ended(&mut self, event_loop_ctx: EventLoopCtx) { self.flags.remove(Flags::KeepsEventLoopAlive); self.flags.insert(Flags::Closed); - // `loop_mut()` — crate-private nonnull-asref accessor; `deactivate` is - // a leaf counter op so the `&mut Loop` borrow does not escape. - self.deactivate(event_loop_ctx.loop_mut()); + // SAFETY: `deactivate` is a leaf counter op, so the `&mut Loop` + // borrow does not escape and no other `&mut Loop` is live. + self.deactivate(unsafe { event_loop_ctx.loop_mut() }); } #[inline] diff --git a/src/io/source.rs b/src/io/source.rs index 47f15fb815ce..a2d5bbbb3dfa 100644 --- a/src/io/source.rs +++ b/src/io/source.rs @@ -213,13 +213,16 @@ impl File { extern "C" fn on_close_complete(fs: *mut uv::fs_t) { // SAFETY: fs points to the .fs field of a Box allocated in open_file(). // Unique ownership: by the time libuv fires this callback the parent has - // detached (fs.data == null) and no Rust `&mut File` is live; this callback - // is the sole owner and reclaims the Box below. - let file = unsafe { &mut *File::from_fs(fs) }; - debug_assert!(file.state == FileState::Closing); - file.fs.deinit(); - // SAFETY: file was allocated via Box::new in open_file(); reclaim and drop. - drop(unsafe { bun_core::heap::take(file as *mut File) }); + // detached (fs.data == null) and this callback is the sole owner. + let file = unsafe { bun_core::heap::take(File::from_fs(fs)) }; + file.finish_close(); + } + + /// Terminal step of the close state machine: the callback owns the `Box`, + /// so deinit the request and free the allocation by dropping `self`. + fn finish_close(mut self: Box) { + debug_assert!(self.state == FileState::Closing); + self.fs.deinit(); } } diff --git a/src/io/windows_event_loop.rs b/src/io/windows_event_loop.rs index 572945b71d8a..c05b0091cb74 100644 --- a/src/io/windows_event_loop.rs +++ b/src/io/windows_event_loop.rs @@ -179,12 +179,10 @@ impl FilePoll { } pub fn deinit_with_vm(&mut self, vm: EventLoopCtx) { - // `loop_mut()` — crate-private nonnull-asref accessor (single deref in - // `EventLoopCtx`); the uws loop is a disjoint allocation from `self`. - // Stacked-Borrows: `self` may live inside `Store.hive`'s inline buffer, - // so `&mut Store` is materialised only *after* `&mut self` is retired - // inside `deinit_possibly_defer` (via `file_polls_mut()`). - let loop_ = vm.loop_mut(); + // SAFETY: the uws loop is a disjoint allocation from `self`, and + // `deinit_possibly_defer` never re-derives it (it only reaches the + // `Store` back-pointer), so `loop_` stays the sole live `&mut Loop`. + let loop_ = unsafe { vm.loop_mut() }; self.deinit_possibly_defer(vm, loop_); } @@ -210,7 +208,7 @@ impl FilePoll { pub fn deactivate(&mut self, loop_: &mut WindowsLoop) { debug_assert!(self.flags.contains(Flags::HasIncrementedPollCount)); loop_.sub_active(self.flags.contains(Flags::HasIncrementedPollCount) as u32); - bun_core::scoped_log!(FilePoll, "deactivate - {}", loop_.uv().active_handles); + bun_core::scoped_log!(FilePoll, "deactivate - {}", loop_.uv().active_handles.get()); self.flags.remove(Flags::HasIncrementedPollCount); } @@ -220,7 +218,7 @@ impl FilePoll { (!self.flags.contains(Flags::Closed) && !self.flags.contains(Flags::HasIncrementedPollCount)) as u32, ); - bun_core::scoped_log!(FilePoll, "activate - {}", loop_.uv().active_handles); + bun_core::scoped_log!(FilePoll, "activate - {}", loop_.uv().active_handles.get()); self.flags.insert(Flags::HasIncrementedPollCount); } @@ -241,8 +239,9 @@ impl FilePoll { pub fn on_ended(&mut self, event_loop_ctx: EventLoopCtx) { self.flags.remove(Flags::KeepsEventLoopAlive); self.flags.insert(Flags::Closed); - // this.deactivate(vm.event_loop_handle.?); - self.deactivate(event_loop_ctx.loop_mut()); + // SAFETY: `deactivate` is a leaf counter op; the borrow does not + // escape and no other `&mut Loop` is live. + self.deactivate(unsafe { event_loop_ctx.loop_mut() }); } /// Prevent a poll from keeping the process alive. @@ -251,8 +250,8 @@ impl FilePoll { return; } bun_core::scoped_log!(FilePoll, "unref"); - // this.deactivate(vm.event_loop_handle.?); - self.deactivate(vm.loop_mut()); + // SAFETY: leaf counter op; the borrow does not escape. + self.deactivate(unsafe { vm.loop_mut() }); } /// Allow a poll to keep the process alive. @@ -262,8 +261,8 @@ impl FilePoll { return; } bun_core::scoped_log!(FilePoll, "ref"); - // this.activate(vm.event_loop_handle.?); - self.activate(event_loop_ctx.loop_mut()); + // SAFETY: leaf counter op; the borrow does not escape. + self.activate(unsafe { event_loop_ctx.loop_mut() }); } } diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs index 1c4d698fb380..d74eabebeedb 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -3025,20 +3025,32 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O // a child of the live `&mut P` at the call site rather than a stale // tag captured here. The transposer shims need no wiring at all — // call sites invoke `P::maybe_transpose_if_*` etc. directly. - self.to_expr_wrapper_namespace = - bun_ast::binding::ToExprWrapper::new(self.arena, |ctx, loc, ref_| { - // SAFETY: `ctx` was derived from the caller's live `&mut P` - // immediately before `Binding::to_expr`; no other `&mut P` - // borrow is active for the duration of this call. - let p = unsafe { &mut *ctx.cast::>() }; - p.wrap_identifier_namespace(loc, ref_) - }); - self.to_expr_wrapper_hoisted = - bun_ast::binding::ToExprWrapper::new(self.arena, |ctx, loc, ref_| { - // SAFETY: same as above. - let p = unsafe { &mut *ctx.cast::>() }; + // Typed trampoline: derefs the erased `ctx` once, then dispatches + // to a safe `&mut P` method. `HOISTED` picks which one. + fn wrap_identifier_tramp( + ctx: *mut core::ffi::c_void, + loc: bun_ast::Loc, + ref_: Ref, + ) -> Expr { + // SAFETY: `ctx` was derived from the caller's live `&mut P` + // immediately before `Binding::to_expr`; no other `&mut P` + // borrow is active for the duration of this call. + let p = unsafe { &mut *ctx.cast::>() }; + if HOISTED { p.wrap_identifier_hoisting(loc, ref_) - }); + } else { + p.wrap_identifier_namespace(loc, ref_) + } + } + + self.to_expr_wrapper_namespace = bun_ast::binding::ToExprWrapper::new( + self.arena, + wrap_identifier_tramp::, + ); + self.to_expr_wrapper_hoisted = bun_ast::binding::ToExprWrapper::new( + self.arena, + wrap_identifier_tramp::, + ); } { diff --git a/src/js_parser/scan/scan_imports.rs b/src/js_parser/scan/scan_imports.rs index 004ddef5bf81..ff1ea73fc71d 100644 --- a/src/js_parser/scan/scan_imports.rs +++ b/src/js_parser/scan/scan_imports.rs @@ -3,8 +3,8 @@ use crate::lower::lower_esm_exports_hmr::ConvertESMExportsForHmr; use crate::p::P; use crate::parser::{ImportItemForNamespaceMap, Ref}; +use bun_ast::import_record; use bun_ast::{self as js_ast, Expr, G, LocRef, S, Stmt, Symbol}; -use bun_ast::{ImportRecord, import_record}; use bun_collections::VecExt; use bun_core::strings; use bun_crash_handler::handle_oom::handle_oom; @@ -61,16 +61,12 @@ impl<'a> ImportScanner<'a> { let import_record_index = st.import_record_index; // We can't keep a long-lived `&mut ImportRecord` for the whole arm - // alongside other `p.*` borrows. We take a raw pointer once and unsafe-deref - // at each use site (no operation below grows `p.import_records`, so - // the pointer stays valid for this iteration). - let record: *mut ImportRecord = - &raw mut p.import_records.items_mut()[import_record_index as usize]; + // alongside other `p.*` borrows, so re-borrow it at each use site. Every + // use is a standalone statement, and the other `p.*` borrows live across + // them (`p.symbols`, `p.import_items_for_namespace`) are disjoint fields. macro_rules! record { () => { - // SAFETY: `record` points into `p.import_records`' backing storage; - // nothing in this match arm reallocates that list. - unsafe { &mut *record } + p.import_records.items_mut()[import_record_index as usize] }; } diff --git a/src/js_parser/visit/mod.rs b/src/js_parser/visit/mod.rs index 6bea16e7e04a..7b79922d27b3 100644 --- a/src/js_parser/visit/mod.rs +++ b/src/js_parser/visit/mod.rs @@ -265,9 +265,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let len = decls.len(); let mut i: usize = 0; 'outer: while i < len { - // SAFETY: i < len; we need disjoint borrows of decls[i] (read/mutate) - // and decls[j] (write at end). j <= i always holds. - let decl: &mut G::Decl = unsafe { &mut *decls.as_mut_ptr().add(i) }; + let decl: &mut G::Decl = &mut decls[i]; i += 1; self.visit_binding(decl.binding, None); diff --git a/src/js_parser_jsc/Macro.rs b/src/js_parser_jsc/Macro.rs index fa0258b6e9e9..2a794b784077 100644 --- a/src/js_parser_jsc/Macro.rs +++ b/src/js_parser_jsc/Macro.rs @@ -40,14 +40,11 @@ pub(crate) fn is_macro_path(str: &[u8]) -> bool { // MacroContext // ══════════════════════════════════════════════════════════════════════════ -// All three are modelled as raw pointers because the referents live -// inside the owning `Transpiler` and are also reachable through other aliases -// (`Transpiler.resolver`, `Transpiler.env`, `Transpiler.options`); a `&'a mut` -// here would forbid that aliasing under stacked-borrows. The `'static` -// erasure on `Resolver`/`DotEnvLoader` matches the `Transpiler<'static>` -// stored in `VirtualMachine` (the only producer of `MacroContext`). +// `env` is a raw pointer because the referent lives inside the owning +// `Transpiler` and is also reachable through `Transpiler.env`. The `'static` +// erasure matches the `Transpiler<'static>` stored in `VirtualMachine`. pub struct MacroContext { - pub resolver: *mut Resolver<'static>, + pub resolver: bun_ptr::ParentRef>, pub env: *mut DotEnvLoader<'static>, pub macros: MacroMap, pub remap: bun_ptr::BackRef, @@ -87,7 +84,9 @@ impl MacroContext { pub fn init(transpiler: &mut Transpiler<'static>) -> MacroContext { MacroContext { macros: MacroMap::new(), - resolver: &raw mut transpiler.resolver, + // SAFETY: `transpiler` outlives every `MacroContext` it produces; + // `from_raw_mut` keeps the write provenance `assume_mut` needs. + resolver: unsafe { bun_ptr::ParentRef::from_raw_mut(&raw mut transpiler.resolver) }, env: transpiler.env, remap: bun_ptr::BackRef::new(&transpiler.options.macro_remap), javascript_object: JSValue::ZERO, @@ -116,10 +115,6 @@ impl MacroContext { debug_assert!(!is_macro_path(import_record_path_without_macro_prefix)); - // SAFETY: `resolver` outlives `self` (see struct comment); uniquely - // accessed for the duration of this resolve call. - let resolver = unsafe { &mut *self.resolver }; - let input_specifier: &[u8] = 'brk: { if let Some(replacement) = ModuleLoader::HardcodedModule::Alias::get( import_record_path, @@ -129,7 +124,9 @@ impl MacroContext { break 'brk replacement.path.as_bytes(); } - let resolve_result = match resolver.resolve( + // SAFETY: the parent `Resolver` outlives `self`, and the exclusive + // borrow is confined to this call, which never re-enters JS. + let resolve_result = match unsafe { self.resolver.assume_mut() }.resolve( source_dir, import_record_path_without_macro_prefix, bun_ast::ImportKind::Stmt, @@ -177,6 +174,7 @@ impl MacroContext { &mut specifier_buf_len, ); + let resolver = self.resolver; let macro_entry = self.macros.get_or_put(hash).expect("unreachable"); if !macro_entry.found_existing { *macro_entry.value_ptr = match Macro::init( @@ -401,7 +399,7 @@ impl Macro { pub fn init( // allocator param deleted — always default_allocator - resolver: &mut Resolver<'static>, + resolver: bun_ptr::ParentRef>, input_specifier: &[u8], log: &mut Log, env: *mut DotEnvLoader<'static>, @@ -468,7 +466,7 @@ impl Macro { Ok(Macro { vm: NonNull::new(vm), - resolver: Some(NonNull::from(resolver)), + resolver: NonNull::new(resolver.as_mut_ptr()), resolved: ResolveResult::default(), disabled: false, }) diff --git a/src/js_printer/renamer.rs b/src/js_printer/renamer.rs index 16487df4a141..54a7351d8e62 100644 --- a/src/js_printer/renamer.rs +++ b/src/js_printer/renamer.rs @@ -564,24 +564,42 @@ impl NumberRenamer { } pub fn assign_name(&mut self, scope: &mut NumberScope, input_ref: Ref) { - let ref_ = self.symbols.follow(input_ref); + let Self { + symbols, + names, + arena, + .. + } = self; + Self::assign_name_in(symbols, names, arena, scope, input_ref); + } + + /// The body of [`Self::assign_name`], written against the individual fields + /// it needs so that a caller holding `&mut self.root` can reach it. + fn assign_name_in( + symbols: &symbol::Map, + names: &mut [Vec], + arena: &Bump, + scope: &mut NumberScope, + input_ref: Ref, + ) { + let ref_ = symbols.follow(input_ref); // Don't rename the same symbol more than once - let inner: &mut Vec = &mut self.names[ref_.source_index() as usize]; + let inner: &mut Vec = &mut names[ref_.source_index() as usize]; if inner.len() > ref_.inner_index() as usize && inner[ref_.inner_index() as usize].len() > 0 { return; } // Don't rename unbound symbols, symbols marked as reserved names, labels, or private names - let symbol: &Symbol = self.symbols.get_const(ref_).unwrap(); + let symbol: &Symbol = symbols.get_const(ref_).unwrap(); if symbol.slot_namespace() != SlotNamespace::Default { return; } // SAFETY: `original_name` is an AST-arena slice that outlives the renamer. let original_name: &[u8] = symbol.original_name.slice(); - let name: NameStr = match scope.find_unused_name(&self.arena, original_name) { + let name: NameStr = match scope.find_unused_name(arena, original_name) { UnusedName::Renamed(name) => name, UnusedName::NoCollision => symbol.original_name, }; @@ -756,13 +774,14 @@ impl NumberRenamer { } pub fn add_top_level_symbol(&mut self, ref_: Ref) { - // Reshaped for borrowck — root is a field of self, but `assign_name` - // needs `&mut self` AND `&mut self.root` simultaneously. Sound only - // while `assign_name` never reaches `self.root` through `self`; keep - // that invariant if `assign_name` changes. - let root: *mut NumberScope = &raw mut self.root; - // SAFETY: assign_name does not touch self.root through `self` - self.assign_name(unsafe { &mut *root }, ref_); + let Self { + symbols, + names, + root, + arena, + .. + } = self; + Self::assign_name_in(symbols, names, arena, root, ref_); } pub fn add_top_level_declared_symbols( diff --git a/src/jsc/ConcurrentPromiseTask.rs b/src/jsc/ConcurrentPromiseTask.rs index 62aba68d025b..c7ad255b2649 100644 --- a/src/jsc/ConcurrentPromiseTask.rs +++ b/src/jsc/ConcurrentPromiseTask.rs @@ -77,15 +77,14 @@ impl<'a, Context: ConcurrentPromiseTaskContext> ConcurrentPromiseTask<'a, Contex } pub unsafe fn run_from_thread_pool(task: *mut WorkPoolTask) { - // SAFETY: only reachable via `WorkPoolTask::callback` (unsafe-fn-ptr - // slot — safe-fn coerces) for the `task` field initialised in - // `create_on_js_thread`; the WorkPool calls back with exactly that - // field, so `from_task_ptr` recovers the live heap `Self` parent, - // exclusively owned by the work pool for this callback's duration. + // SAFETY: only reachable via `WorkPoolTask::callback` for the `task` + // field initialised in `create_on_js_thread`, so `from_task_ptr` + // recovers the live heap `Self`, owned by the pool for this callback. let this = unsafe { Self::from_task_ptr(task) }; - // SAFETY: `this` is alive for the duration of the thread-pool callback; - // exclusively owned by the work pool at this point. - unsafe { (*this).ctx.run() }; + // SAFETY: `this` is the live heap `Self` recovered above. The single + // deref; the borrow ends before `on_finish` publishes `this` to the JS + // thread's concurrent queue. + unsafe { &mut *this }.ctx.run(); Self::on_finish(this); } @@ -101,32 +100,22 @@ impl<'a, Context: ConcurrentPromiseTaskContext> ConcurrentPromiseTask<'a, Contex } fn on_finish(this: *mut Self) { - // SAFETY: only called from `run_from_thread_pool` above with the live - // heap allocation recovered via `from_field_ptr!`. - // `concurrent_task` is an intrusive field of `*this`; `from` - // re-initializes it in place and returns the same address. Passing - // `this` while holding `&mut *this` is sound because `from` only stores - // the pointer (does not dereference it). - let this_ref = unsafe { &mut *this }; - let event_loop = this_ref.event_loop; - let task = core::ptr::NonNull::from( - this_ref - .concurrent_task - .from(this, AutoDeinit::ManualDeinit), - ); - // `task` is the live `concurrent_task` field of the heap-allocated - // job; the queue takes ownership of its intrusive `next` link. + // SAFETY: only called from `run_from_thread_pool` with the live heap + // allocation. Only the intrusive field is borrowed: `from` stores + // `this` without dereferencing it, so `this` stays valid for the queue. + let event_loop = unsafe { (*this).event_loop }; + // SAFETY: same invariant — only the intrusive `concurrent_task` field is + // borrowed, and `from` stores `this` without dereferencing it. + let concurrent_task = unsafe { &mut (*this).concurrent_task }; + let task = core::ptr::NonNull::from(concurrent_task.from(this, AutoDeinit::ManualDeinit)); + // No borrow of `*this` is live here: once enqueued, the JS thread may + // run and free the job before this call returns. event_loop.enqueue_task_concurrent(task); } /// Frees the heap allocation backing this task. - /// - /// # Safety - /// `this` must have been produced by `heap::alloc` (via [`create_on_js_thread`] / - /// the `.manual_deinit` concurrent-task path) and must not be used afterwards. - pub unsafe fn destroy(this: *mut Self) { - // `promise.deinit()` is handled by `JSPromiseStrong: Drop`. - // SAFETY: caller contract above. - drop(unsafe { bun_core::heap::take(this) }); + /// `promise.deinit()` is handled by `JSPromiseStrong: Drop`. + pub fn destroy(self: Box) { + drop(self); } } diff --git a/src/jsc/Debugger.rs b/src/jsc/Debugger.rs index 643f516cb7cd..2e264107da84 100644 --- a/src/jsc/Debugger.rs +++ b/src/jsc/Debugger.rs @@ -106,11 +106,12 @@ pub struct Debugger { // `'static` is genuine: borrowed from process-lifetime env-var storage; // default `""`. pub from_environment_variable: &'static [u8], - pub script_execution_context_id: u32, + /// Mutated through a shared `&Debugger` on the JS thread; `Cell` because + /// reentrant JS may independently borrow the VM while this is live. + pub script_execution_context_id: Cell, pub next_debugger_id: u64, - pub poll_ref: KeepAlive, - pub wait_for_connection: Wait, - // wait_for_connection: bool = false, + pub poll_ref: Cell, + pub wait_for_connection: Cell, pub set_breakpoint_on_first_line: bool, pub mode: Mode, @@ -120,7 +121,7 @@ pub struct Debugger { /// provide the interior mutability. JS-thread only. pub extension_agent: ErasedAgentSlot, pub http_server_agent: HTTPServerAgent, - pub must_block_until_connected: bool, + pub must_block_until_connected: Cell, } impl Default for Debugger { @@ -128,17 +129,17 @@ impl Default for Debugger { Self { path_or_port: None, from_environment_variable: b"", - script_execution_context_id: 0, + script_execution_context_id: Cell::new(0), next_debugger_id: 1, - poll_ref: KeepAlive::default(), - wait_for_connection: Wait::Off, + poll_ref: Cell::new(KeepAlive::default()), + wait_for_connection: Cell::new(Wait::Off), set_breakpoint_on_first_line: false, mode: Mode::Listen, test_reporter_agent: TestReporterAgent::default(), lifecycle_reporter_agent: LifecycleAgent::default(), extension_agent: ErasedAgentSlot::default(), http_server_agent: HTTPServerAgent::default(), - must_block_until_connected: false, + must_block_until_connected: Cell::new(false), } } } @@ -162,16 +163,26 @@ static FUTEX_ATOMIC: AtomicU32 = AtomicU32::new(0); pub(crate) static HAS_CREATED_DEBUGGER: AtomicBool = AtomicBool::new(false); impl Debugger { + /// `poll_ref.ref_()` through the `Cell`. `KeepAlive` is not `Copy`, so + /// take-mutate-restore; the call never runs JS. + #[inline] + pub fn poll_ref_ref(&self, ctx: bun_io::EventLoopCtx) { + let mut keep_alive = self.poll_ref.take(); + keep_alive.ref_(ctx); + self.poll_ref.set(keep_alive); + } + + /// `poll_ref.unref()` through the `Cell`. See [`Self::poll_ref_ref`]. + #[inline] + pub fn poll_ref_unref(&self, ctx: bun_io::EventLoopCtx) { + let mut keep_alive = self.poll_ref.take(); + keep_alive.unref(ctx); + self.poll_ref.set(keep_alive); + } + /// `Debugger.waitForDebuggerIfNecessary(vm)` — block on the futex until /// `start()` (debugger thread) signals, then run the wait-loop until a /// frontend connects (`Debugger__didConnect`) or the deadline elapses. - /// - /// Aliasing: `this.debugger` is read through a raw pointer - /// with fresh short-lived borrows because `event_loop().tick()` / - /// `auto_tick_active()` re-enter JS, which calls `VirtualMachine::get()` - /// and may form independent `&mut VirtualMachine` borrows. Holding a - /// long-lived `&mut Debugger` (which borrows from `&mut VirtualMachine`) - /// across those calls is UB. pub fn wait_for_debugger_if_necessary(this: *mut VirtualMachine) { // `this` is the live per-thread VM; same allocation as // `VirtualMachine::get()` — route through the safe thread-local @@ -181,18 +192,21 @@ impl Debugger { debug_assert!(core::ptr::eq(this, VirtualMachine::get_mut_ptr())); let _ = this; // release: param otherwise unused let this: &VirtualMachine = VirtualMachine::get(); - let Some(dbg) = this.debugger_mut() else { + let Some(dbg) = this.debugger() else { return; }; bun_analytics::features::debugger.fetch_add(1, Ordering::Relaxed); - if !dbg.must_block_until_connected { + if !dbg.must_block_until_connected.get() { return; } - let (ctx_id, wait) = (dbg.script_execution_context_id, dbg.wait_for_connection); + let (ctx_id, wait) = ( + dbg.script_execution_context_id.get(), + dbg.wait_for_connection.get(), + ); // Reset `must_block_until_connected` on every exit path. let _reset = scopeguard::guard((), |()| { - if let Some(d) = this.debugger_mut() { - d.must_block_until_connected = false; + if let Some(d) = this.debugger() { + d.must_block_until_connected.set(false); } }); @@ -249,8 +263,8 @@ impl Debugger { // SAFETY: `vm` is the per-thread singleton; called on the // JS thread (libuv timer callback). Unwinding across // `extern "C"` is UB so we early-return if no debugger. - if let Some(d) = VirtualMachine::get().as_mut().debugger.as_deref_mut() { - d.poll_ref.unref(get_vm_ctx(AllocatorType::Js)); + if let Some(d) = VirtualMachine::get().debugger() { + d.poll_ref_unref(get_vm_ctx(AllocatorType::Js)); } // SAFETY: `handle` is a live `uv_timer_t` (`uv_handle_t` // at offset 0); `deinit_timer` matches `uv_close_cb`. @@ -276,12 +290,9 @@ impl Debugger { } } - // Drop the long-lived `&mut Debugger` before re-entering JS — see - // the aliasing note on this fn. Each loop iteration re-fetches via `debugger_mut()` - // so re-entrant JS may independently borrow the VM. loop { - let wait = match this.debugger.as_deref() { - Some(d) => d.wait_for_connection, + let wait = match this.debugger() { + Some(d) => d.wait_for_connection.get(), None => break, }; if wait == Wait::Off { @@ -289,8 +300,8 @@ impl Debugger { } this.event_loop_mut().tick(); // Re-read after `tick()` — `Debugger__didConnect` may have flipped it. - let wait = match this.debugger.as_deref() { - Some(d) => d.wait_for_connection, + let wait = match this.debugger() { + Some(d) => d.wait_for_connection.get(), None => break, }; match wait { @@ -328,8 +339,8 @@ impl Debugger { let elapsed = bun_core::Timespec::now(bun_core::TimespecMockMode::ForceRealTime); if elapsed.order(&deadline) != core::cmp::Ordering::Less { - if let Some(d) = this.debugger_mut() { - d.poll_ref.unref(get_vm_ctx(AllocatorType::Js)); + if let Some(d) = this.debugger() { + d.poll_ref_unref(get_vm_ctx(AllocatorType::Js)); } bun_core::scoped_log!(debugger, "Timed out waiting for the debugger"); break; @@ -363,9 +374,10 @@ impl Debugger { debug_assert!(core::ptr::eq(this, VirtualMachine::get_mut_ptr())); let this_ref: &VirtualMachine = VirtualMachine::get(); let dbg = this_ref - .debugger_mut() + .debugger() .expect("Debugger::create: vm.debugger is None"); - dbg.script_execution_context_id = Bun__createJSDebugger(global_object); + dbg.script_execution_context_id + .set(Bun__createJSDebugger(global_object)); if !this_ref.has_started_debugger { this_ref.as_mut().has_started_debugger = true; @@ -395,10 +407,10 @@ impl Debugger { this_ref.event_loop_mut().ensure_waker(); // Re-borrow after `ensure_waker` (which may touch `*this`). - let dbg = this_ref.debugger_mut().unwrap(); - if dbg.wait_for_connection != Wait::Off { - dbg.poll_ref.ref_(get_vm_ctx(AllocatorType::Js)); - dbg.must_block_until_connected = true; + let dbg = this_ref.debugger().unwrap(); + if dbg.wait_for_connection.get() != Wait::Off { + dbg.poll_ref_ref(get_vm_ctx(AllocatorType::Js)); + dbg.must_block_until_connected.set(true); } Ok(()) } @@ -491,7 +503,7 @@ impl Debugger { let (ctx_id, is_connect, from_env, path_or_port) = match unsafe { (*other_vm).debugger.as_deref() } { Some(d) => ( - d.script_execution_context_id, + d.script_execution_context_id.get(), d.mode == Mode::Connect, d.from_environment_variable, d.path_or_port, @@ -557,16 +569,15 @@ impl Debugger { // HOST_EXPORT(Debugger__didConnect, c) pub fn did_connect() { - let this = VirtualMachine::get().as_mut(); - // SAFETY: `VirtualMachine::get()` returns the per-thread singleton; called - // on the JS thread. If the debugger is missing we early-return - // defensively (extern "C" — unwinding is UB). - let Some(dbg) = this.debugger.as_deref_mut() else { + let this = VirtualMachine::get(); + // Called on the JS thread; early-return if the debugger is missing + // (extern "C" — unwinding is UB). + let Some(dbg) = this.debugger() else { return; }; - if dbg.wait_for_connection != Wait::Off { - dbg.wait_for_connection = Wait::Off; - dbg.poll_ref.unref(get_vm_ctx(AllocatorType::Js)); + if dbg.wait_for_connection.get() != Wait::Off { + dbg.wait_for_connection.set(Wait::Off); + dbg.poll_ref_unref(get_vm_ctx(AllocatorType::Js)); this.event_loop_mut().wakeup(); } } @@ -820,8 +831,7 @@ impl TestReporterAgent { /// `Inspector::TestReporterAgent*` once the agent is enabled. Caller must /// ensure `is_enabled()` (handle != null). #[inline] - #[allow(clippy::mut_from_ref)] - fn handle_mut(&self) -> &mut TestReporterHandle { + fn handle_mut(&mut self) -> &mut TestReporterHandle { debug_assert!(!self.handle.is_null()); // Caller contract — `is_enabled()` checked; handle is a live C++ heap // allocation owned by the inspector backend. `TestReporterHandle` is an @@ -833,7 +843,7 @@ impl TestReporterAgent { /// /// Since we may have to call .deinit on the name string. pub fn report_test_found( - &self, + &mut self, call_frame: &CallFrame, test_id: i32, name: &mut BunString, @@ -846,13 +856,13 @@ impl TestReporterAgent { } /// Caller must ensure that it is enabled first. - pub fn report_test_start(&self, test_id: i32) { + pub fn report_test_start(&mut self, test_id: i32) { bun_core::scoped_log!(TestReporterAgent, "reportTestStart"); self.handle_mut().report_test_start(test_id); } /// Caller must ensure that it is enabled first. - pub fn report_test_end(&self, test_id: i32, bun_test_status: TestStatus, elapsed: f64) { + pub fn report_test_end(&mut self, test_id: i32, bun_test_status: TestStatus, elapsed: f64) { bun_core::scoped_log!(TestReporterAgent, "reportTestEnd"); self.handle_mut() .report_test_end(test_id, bun_test_status, elapsed); diff --git a/src/jsc/FetchHeaders.rs b/src/jsc/FetchHeaders.rs index 2bec550f166b..f15eb22a8e86 100644 --- a/src/jsc/FetchHeaders.rs +++ b/src/jsc/FetchHeaders.rs @@ -1,4 +1,5 @@ use core::ffi::c_void; +use core::mem::ManuallyDrop; use core::ptr::NonNull; use crate::virtual_machine::VirtualMachine; @@ -6,103 +7,127 @@ use crate::{JSGlobalObject, JSValue, JsResult, VM, host_fn}; use bun_core::{String as BunString, StringPointer, ZigString}; use bun_uws::ResponseKind; -bun_opaque::opaque_ffi! { - /// Opaque C++ `WebCore::FetchHeaders` handle (ref-counted on the C++ side; see `deref`). - pub struct FetchHeaders; +/// The C++ object itself. Only the extern declarations below name this type; +/// all Rust code uses the owning [`FetchHeaders`] handle. +pub mod sys { + bun_opaque::opaque_ffi! { + /// `WebCore::FetchHeaders`. `&Self` is ABI-identical to a non-null + /// `WebCore::FetchHeaders*`, and carries no `noalias`/`readonly` — + /// C++ mutates the header storage through it. + pub struct FetchHeaders; + } } -// `FetchHeaders`/`JSGlobalObject`/`VM` are opaque `UnsafeCell`-backed ZST +// C++ allocates (`new WebCore::FetchHeaders` + `relaxAdoptionRequirement`) and +// hands back a `+1`. One `FetchHeaders` handle owns exactly that one ref. +bun_opaque::foreign_owned!(sys::FetchHeaders, WebCore__FetchHeaders__deref); + +/// Owned handle to a C++ `WebCore::FetchHeaders`. +/// +/// Holds one ref on the C++ intrusive refcount; `Drop` gives it back. Every +/// method takes `&self`: a refcount is shared by definition, and C++ mutates +/// the headers through the same pointer, so there is no `&mut self` to have. +/// +/// A `FetchHeaders` *borrowed* from a JS `Headers` wrapper (see [`Self::cast`]) +/// is a `ManuallyDrop` — the JS object owns that ref, not us. +#[repr(transparent)] +pub struct FetchHeaders(bun_opaque::ForeignRef); + +// `JSGlobalObject`/`VM`/`sys::FetchHeaders` are opaque `UnsafeCell`-backed ZST // handles, so `&T` is ABI-identical to a non-null `*const T` and C++ mutating // header storage / VM state through them is interior mutation invisible to // Rust. `ZigString` and `String` (`BunString`) are plain `#[repr(C)]` PODs; // `&`/`&mut` refs to them at the FFI boundary are sound (C++ reads/writes // only the named struct). // Shims that traffic only in such refs + scalars are declared `safe fn`; those -// that take raw `*mut c_void` / unsized `*mut StringPointer` arrays / `deref` -// (which may free) keep their `unsafe fn` body. +// that take raw `*mut c_void` / unsized `*mut StringPointer` arrays keep their +// `unsafe fn` body. unsafe extern "C" { safe fn WebCore__FetchHeaders__append( - arg0: &FetchHeaders, + arg0: &sys::FetchHeaders, arg1: &ZigString, arg2: &ZigString, arg3: &JSGlobalObject, ); - safe fn WebCore__FetchHeaders__cast_(value0: JSValue, arg1: &VM) -> *mut FetchHeaders; - safe fn WebCore__FetchHeaders__clone(arg0: &FetchHeaders, arg1: &JSGlobalObject) -> JSValue; + safe fn WebCore__FetchHeaders__cast_(value0: JSValue, arg1: &VM) -> *mut sys::FetchHeaders; + safe fn WebCore__FetchHeaders__clone( + arg0: &sys::FetchHeaders, + arg1: &JSGlobalObject, + ) -> JSValue; safe fn WebCore__FetchHeaders__cloneThis( - arg0: &FetchHeaders, + arg0: &sys::FetchHeaders, arg1: &JSGlobalObject, - ) -> *mut FetchHeaders; + ) -> *mut sys::FetchHeaders; fn WebCore__FetchHeaders__copyTo( - arg0: *mut FetchHeaders, + arg0: &sys::FetchHeaders, arg1: *mut StringPointer, arg2: *mut StringPointer, arg3: *mut u8, ); - safe fn WebCore__FetchHeaders__count(arg0: &FetchHeaders, arg1: &mut u32, arg2: &mut u32); - safe fn WebCore__FetchHeaders__createEmpty() -> *mut FetchHeaders; - // safe: `arg0`/`arg1` are opaque handles to C++-owned request structs - // (PicoHeaders / uWS HttpRequest); never dereferenced as Rust data — same - // round-trip contract as `Zig__GlobalObject__resetModuleRegistryMap`. - safe fn WebCore__FetchHeaders__createFromPicoHeaders_(arg0: *const c_void) - -> *mut FetchHeaders; - safe fn WebCore__FetchHeaders__createFromUWS(arg1: *mut c_void) -> *mut FetchHeaders; + safe fn WebCore__FetchHeaders__count(arg0: &sys::FetchHeaders, arg1: &mut u32, arg2: &mut u32); + safe fn WebCore__FetchHeaders__createEmpty() -> *mut sys::FetchHeaders; + // NOT `safe fn`: C++ does `*reinterpret_cast(arg0)` / + // `*reinterpret_cast(arg1)`. Safe Rust can forge a + // `*mut c_void`, so the call itself carries the validity obligation. + fn WebCore__FetchHeaders__createFromPicoHeaders_(arg0: *const c_void) + -> *mut sys::FetchHeaders; + fn WebCore__FetchHeaders__createFromUWS(arg1: *mut c_void) -> *mut sys::FetchHeaders; + // C++ declares `StringPointer*` but only reads `arg1[i]`/`arg2[i]`, so the + // Rust side declares `*const` and passes shared slices. fn WebCore__FetchHeaders__createValueNotJS( - arg0: *const JSGlobalObject, - arg1: *mut StringPointer, - arg2: *mut StringPointer, - arg3: *const ZigString, - arg4: u32, - ) -> *mut FetchHeaders; - fn WebCore__FetchHeaders__createValue( - arg0: *const JSGlobalObject, - arg1: *mut StringPointer, - arg2: *mut StringPointer, - arg3: *const ZigString, + arg0: &JSGlobalObject, + arg1: *const StringPointer, + arg2: *const StringPointer, + arg3: &ZigString, arg4: u32, - ) -> JSValue; - // safe: `FetchHeaders` is an `opaque_ffi!` ZST handle; `&mut` is ABI-identical - // to a non-null `*mut` and the C++ refcount decrement is interior to the cell. - safe fn WebCore__FetchHeaders__deref(arg0: &mut FetchHeaders); - safe fn WebCore__FetchHeaders__fastGet_(arg0: &FetchHeaders, arg1: u8, arg2: &mut ZigString); - safe fn WebCore__FetchHeaders__fastHas_(arg0: &FetchHeaders, arg1: u8) -> bool; - safe fn WebCore__FetchHeaders__fastRemove_(arg0: &FetchHeaders, arg1: u8); + ) -> *mut sys::FetchHeaders; + // safe: C++ takes `FetchHeaders*` and calls the intrusive `->deref()`. A + // refcount decrement is not exclusive access — other refs exist by + // definition — so the receiver is `&`, not `&mut`. + safe fn WebCore__FetchHeaders__deref(arg0: &sys::FetchHeaders); + safe fn WebCore__FetchHeaders__fastGet_( + arg0: &sys::FetchHeaders, + arg1: u8, + arg2: &mut ZigString, + ); + safe fn WebCore__FetchHeaders__fastHas_(arg0: &sys::FetchHeaders, arg1: u8) -> bool; + safe fn WebCore__FetchHeaders__fastRemove_(arg0: &sys::FetchHeaders, arg1: u8); safe fn WebCore__FetchHeaders__get_( - arg0: &FetchHeaders, + arg0: &sys::FetchHeaders, arg1: &ZigString, arg2: &mut ZigString, arg3: &JSGlobalObject, ); safe fn WebCore__FetchHeaders__has( - arg0: &FetchHeaders, + arg0: &sys::FetchHeaders, arg1: &ZigString, arg2: &JSGlobalObject, ) -> bool; - safe fn WebCore__FetchHeaders__isEmpty(arg0: &FetchHeaders) -> bool; + safe fn WebCore__FetchHeaders__isEmpty(arg0: &sys::FetchHeaders) -> bool; safe fn WebCore__FetchHeaders__remove( - arg0: &FetchHeaders, + arg0: &sys::FetchHeaders, arg1: &ZigString, arg2: &JSGlobalObject, ); - safe fn WebCore__FetchHeaders__toJS(arg0: &FetchHeaders, arg1: &JSGlobalObject) -> JSValue; - // safe: `FetchHeaders` is an opaque ZST handle (`&mut` ≡ non-null `*mut`); - // `arg2` is an opaque handle to a C++-owned uWS response (never dereferenced - // as Rust data). - safe fn WebCore__FetchHeaders__toUWSResponse( - arg0: &mut FetchHeaders, + safe fn WebCore__FetchHeaders__toJS(arg0: &sys::FetchHeaders, arg1: &JSGlobalObject) + -> JSValue; + // NOT `safe fn`: C++ does `reinterpret_cast*>(arg2)` / + // `reinterpret_cast(arg0)` and dereferences it. + fn WebCore__FetchHeaders__toUWSResponse( + arg0: &sys::FetchHeaders, kind: ResponseKind, arg2: *mut c_void, ); - safe fn WebCore__FetchHeaders__createFromH3(arg0: *mut c_void) -> *mut FetchHeaders; + fn WebCore__FetchHeaders__createFromH3(arg0: *mut c_void) -> *mut sys::FetchHeaders; safe fn WebCore__FetchHeaders__createFromJS( arg0: &JSGlobalObject, arg1: JSValue, - ) -> *mut FetchHeaders; + ) -> *mut sys::FetchHeaders; safe fn WebCore__FetchHeaders__put( - this: &FetchHeaders, - name_: HTTPHeaderName, + this: &sys::FetchHeaders, + name: HTTPHeaderName, value: &BunString, global: &JSGlobalObject, ); @@ -114,231 +139,278 @@ struct PicoHeaders { len: usize, } -// The 4 forwarding wrappers below pass *mut StringPointer/*mut u8 straight to -// C++ without dereferencing; clippy::not_unsafe_ptr_arg_deref is a false -// positive on opaque-token forwarding through an unsafe extern call. -#[allow(clippy::not_unsafe_ptr_arg_deref)] +/// Ownership plumbing. impl FetchHeaders { - pub fn create_value( - global: &JSGlobalObject, - names: *mut StringPointer, - values: *mut StringPointer, - buf: &ZigString, - count_: u32, - ) -> JSValue { - // SAFETY: forwarding caller-provided buffers to C++; `global` is an opaque ZST handle - // passed by address only — C++ never dereferences it as Rust data. - unsafe { WebCore__FetchHeaders__createValue(global, names, values, buf, count_) } - } - - /// Construct a `Headers` object from a JSValue. - /// - /// This can be: - /// - Array<[String, String]> - /// - Record. + /// Adopt a `+1` returned by C++. /// - /// Throws an exception if invalid. - /// - /// If empty, returns null. - pub fn create_from_js( - global: &JSGlobalObject, - value: JSValue, - ) -> JsResult>> { - host_fn::from_js_host_call_generic(global, || { - NonNull::new(WebCore__FetchHeaders__createFromJS(global, value)) - }) + /// # Safety + /// `ptr` must carry exactly one ref that no other handle will release. + #[inline] + pub unsafe fn adopt(ptr: NonNull) -> Self { + // SAFETY: caller transfers the +1. + Self(unsafe { bun_opaque::ForeignRef::adopt(ptr) }) } - pub fn put_default( - &mut self, - name_: HTTPHeaderName, - value: &BunString, - global: &JSGlobalObject, - ) -> JsResult<()> { - if self.fast_has(name_) { - return Ok(()); - } + /// Adopt a nullable `+1`; `None` on null. + #[inline] + fn adopt_ptr(ptr: *mut sys::FetchHeaders) -> Option { + // SAFETY: C++ `create*` returns a fresh +1 or null. + NonNull::new(ptr).map(|p| unsafe { Self::adopt(p) }) + } - self.put(name_, value, global) + /// The C++ pointer, still owned by `self`. + #[inline] + pub fn as_ptr(&self) -> *mut sys::FetchHeaders { + self.0.as_ptr() } - pub fn create( - global: &JSGlobalObject, - names: *mut StringPointer, - values: *mut StringPointer, - buf: &ZigString, - count_: u32, - ) -> Option> { - // SAFETY: forwarding caller-provided buffers to C++; `global` is an opaque ZST handle - // passed by address only. - let p = - unsafe { WebCore__FetchHeaders__createValueNotJS(global, names, values, buf, count_) }; - NonNull::new(p) + /// Hand our `+1` to a foreign owner. Pairs with a later [`Self::adopt`]. + #[inline] + pub fn leak(self) -> NonNull { + self.0.leak() } - pub fn from( - global: &JSGlobalObject, - names: *mut StringPointer, - values: *mut StringPointer, - buf: &ZigString, - count_: u32, - ) -> JSValue { - // SAFETY: forwarding caller-provided buffers to C++; `global` is an opaque ZST handle - // passed by address only. - unsafe { WebCore__FetchHeaders__createValue(global, names, values, buf, count_) } + #[inline] + fn raw(&self) -> &sys::FetchHeaders { + &self.0 } +} - pub fn is_empty(&mut self) -> bool { - WebCore__FetchHeaders__isEmpty(self) +/// Constructors. C++ allocates; every one of these returns a `+1`. +impl FetchHeaders { + pub fn create_empty() -> Self { + Self::adopt_ptr(WebCore__FetchHeaders__createEmpty()) + .expect("WebCore__FetchHeaders__createEmpty returned null") } - pub fn create_from_uws(uws_request: *mut c_void) -> NonNull { - NonNull::new(WebCore__FetchHeaders__createFromUWS(uws_request)) + /// # Safety + /// `uws_request` must be a live `uWS::HttpRequest*`; C++ dereferences it. + pub unsafe fn create_from_uws(uws_request: *mut c_void) -> Self { + // SAFETY: caller contract. + Self::adopt_ptr(unsafe { WebCore__FetchHeaders__createFromUWS(uws_request) }) .expect("WebCore__FetchHeaders__createFromUWS returned null") } - pub fn create_from_h3(h3_request: *mut c_void) -> NonNull { - NonNull::new(WebCore__FetchHeaders__createFromH3(h3_request)) + /// # Safety + /// `h3_request` must be a live `uWS::Http3Request*`; C++ dereferences it. + pub unsafe fn create_from_h3(h3_request: *mut c_void) -> Self { + // SAFETY: caller contract. + Self::adopt_ptr(unsafe { WebCore__FetchHeaders__createFromH3(h3_request) }) .expect("WebCore__FetchHeaders__createFromH3 returned null") } - pub fn to_uws_response(&mut self, kind: ResponseKind, uws_response: *mut c_void) { - WebCore__FetchHeaders__toUWSResponse(self, kind, uws_response) - } - - pub fn create_empty() -> NonNull { - NonNull::new(WebCore__FetchHeaders__createEmpty()) - .expect("WebCore__FetchHeaders__createEmpty returned null") - } - - pub fn create_from_pico_headers(pico_headers_list: &[T]) -> NonNull { + pub fn create_from_pico_headers(pico_headers_list: &[T]) -> Self { let out = PicoHeaders { ptr: pico_headers_list.as_ptr().cast::(), len: pico_headers_list.len(), }; - // `out` lives across the call; C++ copies the headers synchronously. - NonNull::new(WebCore__FetchHeaders__createFromPicoHeaders_( - std::ptr::from_ref(&out).cast::(), - )) - .expect("WebCore__FetchHeaders__createFromPicoHeaders_ returned null") + // SAFETY: `out` is a live `PicoHeaders`, layout-compatible with C++'s + // `PicoHTTPHeaders`, and lives across the call; C++ copies synchronously. + unsafe { Self::create_from_pico_headers_(std::ptr::from_ref(&out).cast::()) } } - pub fn create_from_pico_headers_(pico_headers: *const c_void) -> NonNull { - NonNull::new(WebCore__FetchHeaders__createFromPicoHeaders_(pico_headers)) + /// # Safety + /// `pico_headers` must point to a live `PicoHeaders`. + unsafe fn create_from_pico_headers_(pico_headers: *const c_void) -> Self { + // SAFETY: caller contract. + Self::adopt_ptr(unsafe { WebCore__FetchHeaders__createFromPicoHeaders_(pico_headers) }) .expect("WebCore__FetchHeaders__createFromPicoHeaders_ returned null") } - pub fn append(&mut self, name_: &ZigString, value: &ZigString, global: &JSGlobalObject) { - WebCore__FetchHeaders__append(self, name_, value, global) + /// Construct from a JSValue: `Array<[String, String]>` or + /// `Record`. Throws on invalid input; `None` if empty. + pub fn create_from_js(global: &JSGlobalObject, value: JSValue) -> JsResult> { + host_fn::from_js_host_call_generic(global, || { + Self::adopt_ptr(WebCore__FetchHeaders__createFromJS(global, value)) + }) + } + + /// `names` and `values` must be parallel: C++ reads `names[i]`/`values[i]` + /// for `i < names.len()`, resolving each against `buf`. + pub fn create( + global: &JSGlobalObject, + names: &[StringPointer], + values: &[StringPointer], + buf: &ZigString, + ) -> Option { + assert_eq!(names.len(), values.len(), "parallel header columns"); + let count = u32::try_from(names.len()).expect("header count exceeds u32"); + // SAFETY: C++ reads exactly `count` entries from each column and does not + // retain the pointers. + let p = unsafe { + WebCore__FetchHeaders__createValueNotJS( + global, + names.as_ptr(), + values.as_ptr(), + buf, + count, + ) + }; + Self::adopt_ptr(p) + } + + /// Deep-copies on the C++ side, so the result is a fresh `+1`. + pub fn clone_this(&self, global: &JSGlobalObject) -> JsResult> { + host_fn::from_js_host_call_generic(global, || { + Self::adopt_ptr(WebCore__FetchHeaders__cloneThis(self.raw(), global)) + }) + } + + /// Borrow the `FetchHeaders` inside a JS `Headers` wrapper. + /// + /// `WebCoreCast` takes **no ref** — the JS object owns it. Hence + /// `ManuallyDrop`: dropping this would release a ref we never took. + pub fn cast_(value: JSValue, vm: &VM) -> Option> { + NonNull::new(WebCore__FetchHeaders__cast_(value, vm)) + // SAFETY: wrapped in ManuallyDrop, so the borrowed ref is never released. + .map(|p| ManuallyDrop::new(unsafe { Self::adopt(p) })) + } + + pub fn cast(value: JSValue) -> Option> { + // SAFETY: `VirtualMachine::get()` is only called from the JS thread, where + // `global` is a live non-null JSGlobalObject for the VM's lifetime. + let global = VirtualMachine::get().global(); + Self::cast_(value, global.vm()) + } +} + +/// Header access. `&self` throughout: C++ mutates through the same pointer. +impl FetchHeaders { + pub fn is_empty(&self) -> bool { + WebCore__FetchHeaders__isEmpty(self.raw()) + } + + pub fn append(&self, name: &ZigString, value: &ZigString, global: &JSGlobalObject) { + WebCore__FetchHeaders__append(self.raw(), name, value, global) } /// `value`'s tag carries its encoding, and a `WTFStringImpl`-tagged value /// is ref'd by the C++ side instead of copied character-by-character. pub fn put( - &mut self, - name_: HTTPHeaderName, + &self, + name: HTTPHeaderName, value: &BunString, global: &JSGlobalObject, ) -> JsResult<()> { host_fn::from_js_host_call_generic(global, || { - WebCore__FetchHeaders__put(self, name_, value, global) + WebCore__FetchHeaders__put(self.raw(), name, value, global) }) } - pub fn get_(&mut self, name_: &ZigString, out: &mut ZigString, global: &JSGlobalObject) { - WebCore__FetchHeaders__get_(self, name_, out, global) + pub fn put_default( + &self, + name: HTTPHeaderName, + value: &BunString, + global: &JSGlobalObject, + ) -> JsResult<()> { + if self.fast_has(name) { + return Ok(()); + } + self.put(name, value, global) } - pub fn get(&mut self, name_: &[u8], global: &JSGlobalObject) -> Option { + fn get_(&self, name: &ZigString, out: &mut ZigString, global: &JSGlobalObject) { + WebCore__FetchHeaders__get_(self.raw(), name, out, global) + } + + pub fn get(&self, name: &[u8], global: &JSGlobalObject) -> Option { let mut out = ZigString::EMPTY; - self.get_(&ZigString::init(name_), &mut out, global); + self.get_(&ZigString::init(name), &mut out, global); if out.len > 0 { // Returns the ZigString view (borrows C++-owned header // storage); caller may `.slice()` it. Returning `&[u8]` directly // would borrow the local `out`, not the underlying buffer. return Some(out); } - None } - pub fn has(&mut self, name_: &ZigString, global: &JSGlobalObject) -> bool { - WebCore__FetchHeaders__has(self, name_, global) + pub fn has(&self, name: &ZigString, global: &JSGlobalObject) -> bool { + WebCore__FetchHeaders__has(self.raw(), name, global) } - pub fn fast_has(&mut self, name_: HTTPHeaderName) -> bool { - self.fast_has_(name_ as u8) + pub fn remove(&self, name: &ZigString, global: &JSGlobalObject) { + WebCore__FetchHeaders__remove(self.raw(), name, global) } - pub fn fast_get(&mut self, name_: HTTPHeaderName) -> Option { - let mut str = ZigString::init(b""); - self.fast_get_(name_ as u8, &mut str); - if str.len == 0 { - return None; - } - - Some(str) + pub fn fast_has(&self, name: HTTPHeaderName) -> bool { + self.fast_has_(name as u8) } - pub fn fast_has_(&mut self, name_: u8) -> bool { - WebCore__FetchHeaders__fastHas_(self, name_) + fn fast_has_(&self, name: u8) -> bool { + WebCore__FetchHeaders__fastHas_(self.raw(), name) } - pub fn fast_get_(&mut self, name_: u8, str: &mut ZigString) { - WebCore__FetchHeaders__fastGet_(self, name_, str) - } - - pub fn fast_remove(&mut self, header: HTTPHeaderName) { - self.fast_remove_(header as u8) - } - - pub fn fast_remove_(&mut self, header: u8) { - WebCore__FetchHeaders__fastRemove_(self, header) - } - - pub fn remove(&mut self, name_: &ZigString, global: &JSGlobalObject) { - WebCore__FetchHeaders__remove(self, name_, global) + pub fn fast_get(&self, name: HTTPHeaderName) -> Option { + let mut out = ZigString::init(b""); + self.fast_get_(name as u8, &mut out); + if out.len == 0 { + return None; + } + Some(out) } - pub fn cast_(value: JSValue, vm: &VM) -> Option> { - NonNull::new(WebCore__FetchHeaders__cast_(value, vm)) + fn fast_get_(&self, name: u8, out: &mut ZigString) { + WebCore__FetchHeaders__fastGet_(self.raw(), name, out) } - pub fn cast(value: JSValue) -> Option> { - // SAFETY: `VirtualMachine::get()` is only called from the JS thread, where - // `global` is a live non-null JSGlobalObject for the VM's lifetime. - let global = VirtualMachine::get().global(); - Self::cast_(value, global.vm()) + pub fn fast_remove(&self, header: HTTPHeaderName) { + self.fast_remove_(header as u8) } - pub fn to_js(&mut self, global_this: &JSGlobalObject) -> JSValue { - WebCore__FetchHeaders__toJS(self, global_this) + fn fast_remove_(&self, header: u8) { + WebCore__FetchHeaders__fastRemove_(self.raw(), header) } - pub fn count(&mut self, names: &mut u32, buf_len: &mut u32) { - WebCore__FetchHeaders__count(self, names, buf_len) + /// `(header_count, buf_len)` — the sizes [`Self::copy_to`] expects. + pub fn count(&self) -> (u32, u32) { + let (mut header_count, mut buf_len) = (0u32, 0u32); + WebCore__FetchHeaders__count(self.raw(), &mut header_count, &mut buf_len); + (header_count, buf_len) } - pub fn clone(&mut self, global: &JSGlobalObject) -> JSValue { - WebCore__FetchHeaders__clone(self, global) + /// Writes one `StringPointer` per header into the parallel `names`/`values` + /// columns and the name/value bytes into `buf`. + /// + /// All three must be sized from a prior [`Self::count`]: C++ writes + /// `header_count` entries with no bounds check. `count` is not re-read here + /// — it walks the C++ header iterator, and the caller already paid for it. + pub fn copy_to( + &self, + names: &mut [StringPointer], + values: &mut [StringPointer], + buf: &mut [u8], + ) { + debug_assert_eq!(names.len(), values.len(), "parallel header columns"); + // SAFETY: caller sized all three from `count()`. + unsafe { + WebCore__FetchHeaders__copyTo( + self.raw(), + names.as_mut_ptr(), + values.as_mut_ptr(), + buf.as_mut_ptr(), + ) + } } +} - pub fn clone_this( - &mut self, - global: &JSGlobalObject, - ) -> JsResult>> { - host_fn::from_js_host_call_generic(global, || { - NonNull::new(WebCore__FetchHeaders__cloneThis(self, global)) - }) +/// Conversions to JS. +impl FetchHeaders { + pub fn to_js(&self, global_this: &JSGlobalObject) -> JSValue { + WebCore__FetchHeaders__toJS(self.raw(), global_this) } - pub fn deref(&mut self) { - WebCore__FetchHeaders__deref(self) + /// Shallow-copies into a new JS `Headers` object; does not clone `self`. + pub fn clone(&self, global: &JSGlobalObject) -> JSValue { + WebCore__FetchHeaders__clone(self.raw(), global) } - pub fn copy_to(&mut self, names: *mut StringPointer, values: *mut StringPointer, buf: *mut u8) { - // SAFETY: caller guarantees names/values/buf are sized per a prior `count()` call - unsafe { WebCore__FetchHeaders__copyTo(self, names, values, buf) } + /// # Safety + /// `uws_response` must be a live `uWS::HttpResponse*` / `uWS::Http3Response*` + /// matching `kind`; C++ dereferences it. + pub unsafe fn to_uws_response(&self, kind: ResponseKind, uws_response: *mut c_void) { + // SAFETY: caller contract. + unsafe { WebCore__FetchHeaders__toUWSResponse(self.raw(), kind, uws_response) } } } diff --git a/src/jsc/JSMap.rs b/src/jsc/JSMap.rs index 522756df9f0b..e4292ca2d043 100644 --- a/src/jsc/JSMap.rs +++ b/src/jsc/JSMap.rs @@ -21,7 +21,7 @@ impl JSMap { } #[track_caller] - pub fn set(&mut self, global: &JSGlobalObject, key: JSValue, value: JSValue) -> JsResult<()> { + pub fn set(&self, global: &JSGlobalObject, key: JSValue, value: JSValue) -> JsResult<()> { crate::cpp::JSC__JSMap__set(self, global, key, value) } @@ -30,31 +30,31 @@ impl JSMap { /// Note this shares semantics with the JS `Map.prototype.get` method, and /// will return `JSValue::UNDEFINED` if a value is not found. #[track_caller] - pub fn get(&mut self, global: &JSGlobalObject, key: JSValue) -> JsResult { + pub fn get(&self, global: &JSGlobalObject, key: JSValue) -> JsResult { crate::cpp::JSC__JSMap__get(self, global, key) } /// Test whether this JS Map object has a given key. #[track_caller] - pub fn has(&mut self, global: &JSGlobalObject, key: JSValue) -> JsResult { + pub fn has(&self, global: &JSGlobalObject, key: JSValue) -> JsResult { crate::cpp::JSC__JSMap__has(self, global, key) } /// Attempt to remove a key from this JS Map object. #[track_caller] - pub fn remove(&mut self, global: &JSGlobalObject, key: JSValue) -> JsResult { + pub fn remove(&self, global: &JSGlobalObject, key: JSValue) -> JsResult { crate::cpp::JSC__JSMap__remove(self, global, key) } /// Clear all entries from this JS Map object. #[track_caller] - pub fn clear(&mut self, global: &JSGlobalObject) -> JsResult<()> { + pub fn clear(&self, global: &JSGlobalObject) -> JsResult<()> { crate::cpp::JSC__JSMap__clear(self, global) } /// Retrieve the number of entries in this JS Map object. #[track_caller] - pub fn size(&mut self, global: &JSGlobalObject) -> JsResult { + pub fn size(&self, global: &JSGlobalObject) -> JsResult { crate::cpp::JSC__JSMap__size(self, global) } diff --git a/src/jsc/JSPromise.rs b/src/jsc/JSPromise.rs index 72b9b2cfcd18..54a947eafd66 100644 --- a/src/jsc/JSPromise.rs +++ b/src/jsc/JSPromise.rs @@ -58,9 +58,9 @@ unsafe extern "C" { ) -> JSValue; safe fn JSC__JSPromise__status(this: &JSPromise) -> u32; - safe fn JSC__JSPromise__result(this: &mut JSPromise, vm: &VM) -> JSValue; + safe fn JSC__JSPromise__result(this: &JSPromise, vm: &VM) -> JSValue; safe fn JSC__JSPromise__isHandled(this: &JSPromise) -> bool; - safe fn JSC__JSPromise__setHandled(this: &mut JSPromise); + safe fn JSC__JSPromise__setHandled(this: &JSPromise); // The resolve/reject/rejectAsHandled shims are `void` on the C side // (bindings.cpp) — there is no bool sentinel on the wire; a pending // exception is surfaced by checking `global.has_exception()` after the @@ -117,24 +117,6 @@ impl Weak { } } - /// Borrow the GC-rooted `JSPromise` cell. Panics if the weak slot is empty - /// or no longer a promise. - /// - /// Safe because `JSPromise` is an `opaque_ffi!` ZST handle: a `&mut` to it - /// covers zero bytes (see [`bun_opaque::opaque_deref_mut`] for the proof), - /// so two callers cannot alias any Rust-visible memory. The pointer comes - /// from the JSValue payload (not derived from `&self`) and the weak ref - /// keeps the cell observable while held. - pub fn get(&self) -> &mut JSPromise { - JSPromise::opaque_mut(self.weak.get().unwrap().as_promise().unwrap()) - } - - /// See [`get`]; returns `None` instead of panicking when the slot is empty. - pub fn get_or_null(&self) -> Option<&mut JSPromise> { - let promise_value = self.weak.get()?; - promise_value.as_promise().map(JSPromise::opaque_mut) - } - pub fn value(&self) -> JSValue { self.weak.get().unwrap() } @@ -389,7 +371,10 @@ impl JSPromise { JSPromise::opaque_ref(p).status() } - pub fn result(&mut self, vm: &VM) -> JSValue { + /// `&self`, not `&mut self`: the cell is GC-owned and the C++ side takes a + /// bare `JSPromise*`. A `&mut` here would be stacked with any re-entrant + /// host→JS→host borrow of the same promise. + pub fn result(&self, vm: &VM) -> JSValue { JSC__JSPromise__result(self, vm) } @@ -397,7 +382,8 @@ impl JSPromise { JSC__JSPromise__isHandled(self) } - pub fn set_handled(&mut self) { + /// `&self` for the same reason as [`Self::result`]. + pub fn set_handled(&self) { JSC__JSPromise__setHandled(self) } diff --git a/src/jsc/MarkedArgumentBuffer.rs b/src/jsc/MarkedArgumentBuffer.rs index a7520a127f77..e5e8bb4fc1ec 100644 --- a/src/jsc/MarkedArgumentBuffer.rs +++ b/src/jsc/MarkedArgumentBuffer.rs @@ -28,16 +28,13 @@ impl MarkedArgumentBuffer { f: Option, r: Option, } - extern "C" fn run(ctx: *mut Ctx, args: *mut MarkedArgumentBuffer) + extern "C" fn run(ctx: &mut Ctx, args: &mut MarkedArgumentBuffer) where F: FnOnce(&mut MarkedArgumentBuffer) -> R, { - // SAFETY: `ctx` is the `&mut ctx` passed to `run` below. - let ctx = unsafe { &mut *ctx }; let f = ctx.f.take().unwrap(); - // SAFETY: `args` is the live stack-allocated `MarkedArgumentBuffer` C++ - // hands us for the duration of this callback. - ctx.r = Some(f(unsafe { &mut *args })); + let r = f(args); + ctx.r = Some(r); } let mut ctx = Ctx { f: Some(f), @@ -51,7 +48,7 @@ impl MarkedArgumentBuffer { MarkedArgumentBuffer__append(self, value) } - pub fn run(ctx: &mut T, func: extern "C" fn(ctx: *mut T, args: *mut MarkedArgumentBuffer)) { + pub fn run(ctx: &mut T, func: extern "C" fn(ctx: &mut T, args: &mut MarkedArgumentBuffer)) { // `MarkedArgumentBuffer__run` round-trips `ctx` opaquely back to `func`, // and `func`'s ABI is identical modulo the // pointee types (both params are thin pointers). @@ -61,7 +58,7 @@ impl MarkedArgumentBuffer { // thin-pointer params; ABI-identical modulo pointee type. unsafe { bun_ptr::cast_fn_ptr::< - extern "C" fn(*mut T, *mut MarkedArgumentBuffer), + extern "C" fn(&mut T, &mut MarkedArgumentBuffer), extern "C" fn(*mut c_void, *mut c_void), >(func) }, @@ -85,15 +82,12 @@ macro_rules! marked_argument_buffer_wrap { callframe: &'a $crate::CallFrame, } extern "C" fn run( - this: *mut Context<'_>, - marked_argument_buffer: *mut $crate::MarkedArgumentBuffer, + this: &mut Context<'_>, + marked_argument_buffer: &mut $crate::MarkedArgumentBuffer, ) { - // SAFETY: `this` is the `&mut ctx` passed to `MarkedArgumentBuffer::run` below; - // `marked_argument_buffer` is the live stack-allocated buffer C++ hands us. - let this = unsafe { &mut *this }; - this.result = $function(this.global_this, this.callframe, unsafe { - &mut *marked_argument_buffer - }); + let (global_this, callframe) = (this.global_this, this.callframe); + let result = $function(global_this, callframe, marked_argument_buffer); + this.result = result; } let mut ctx = Context { diff --git a/src/jsc/PosixSignalHandle.rs b/src/jsc/PosixSignalHandle.rs index 4a1390268b1f..35ef5d487784 100644 --- a/src/jsc/PosixSignalHandle.rs +++ b/src/jsc/PosixSignalHandle.rs @@ -161,8 +161,9 @@ pub(crate) extern "C" fn Bun__ensureSignalHandler() { #[cfg(unix)] { if let Some(vm) = VirtualMachine::get_main_thread_vm() { - // SAFETY: `vm` and its event loop are process-lifetime. - let this = unsafe { &mut *(*vm).event_loop() }; + // SAFETY: `vm` is process-lifetime; `event_loop_mut()` is the + // audited accessor for the VM-owned event loop. + let this = unsafe { (*vm).event_loop_mut() }; if this.signal_handler.is_none() { let boxed = PosixSignalHandle::new(PosixSignalHandle::default()); this.signal_handler = diff --git a/src/jsc/RuntimeTranspilerStore.rs b/src/jsc/RuntimeTranspilerStore.rs index d9924856527d..5ef768d0ca50 100644 --- a/src/jsc/RuntimeTranspilerStore.rs +++ b/src/jsc/RuntimeTranspilerStore.rs @@ -31,6 +31,7 @@ use bun_threading::unbounded_queue::{self, UnboundedQueue}; use bun_threading::work_pool::{Task as WorkPoolTask, WorkPool}; use bun_watcher::Watcher; +use crate::JsCell; use crate::async_module::AsyncModule; use crate::event_loop::{ConcurrentTask, EventLoop}; use crate::hot_reloader::ImportWatcher; @@ -456,29 +457,26 @@ impl Fetcher { // wrapper showed up on the // async-import hot path. Const-init `Cell` (no dtor). #[thread_local] -static SOURCE_CODE_PRINTER: Cell>> = Cell::new(None); +static SOURCE_CODE_PRINTER: Cell>>> = Cell::new(None); -/// Get-or-leak accessor for the `#[thread_local]` `Cell>>` -/// slot above. Returns `&'static mut T` because the Box is leaked for the -/// worker thread's lifetime and `#[thread_local]` guarantees per-thread -/// exclusive access; callers reborrow `&'static T` where a shared ref suffices. +/// Get-or-leak accessor for the `#[thread_local]` slot above. Returns +/// `&'static JsCell`: the Box is leaked for the worker thread's lifetime, +/// and the cell supplies the `&self` mutation callers need without ever +/// handing out a `&mut T` that could alias the writeback guard's pointer. #[inline] fn tls_get_or_leak( - slot: &Cell>>, - init: impl FnOnce() -> Box, -) -> &'static mut T { + slot: &Cell>>>, + init: impl FnOnce() -> Box>, +) -> &'static JsCell { let p = slot.get().unwrap_or_else(|| { let p = bun_core::heap::into_raw_nn(init()); slot.set(Some(p)); p }); - // SAFETY: `p` is the `NonNull` produced by `heap::into_raw_nn(Box)` - // (either just now or on a prior call) and never freed — the slot is a - // per-worker-thread leak. `#[thread_local]` storage means only this thread - // ever observes `p`, and every borrow returned here is scoped to one - // `TranspilerJob::run()` activation (no `&T`/`&mut T` from a prior call - // survives), so the `&mut` is exclusive for its actual use. - unsafe { &mut *p.as_ptr() } + // SAFETY: `p` is the `NonNull` produced by `heap::into_raw_nn` (either just + // now or on a prior call) and never freed — the slot is a per-worker-thread + // leak, so the pointee outlives every `'static` borrow handed out here. + unsafe { p.as_ref() } } impl TranspilerJob { @@ -808,7 +806,7 @@ impl TranspilerJob { // raw-pointer laundering (which the unused-assignment lint can't see). let should_close_input_file_fd = Cell::new(fd.is_none()); - let mut input_file_fd: Fd = Fd::INVALID; + let input_file_fd: Cell = Cell::new(Fd::INVALID); // SAFETY: leaf scalar field reads on `*vm`; see `vm` note above. let (vm_main, vm_main_hash) = unsafe { ((*vm).main(), (*vm).main_hash) }; @@ -828,10 +826,7 @@ impl TranspilerJob { loader, dirname_fd: Fd::INVALID, file_descriptor: fd, - // SAFETY: `input_file_fd` is a stack local declared above and - // outlives `parse_options`; `addr_of_mut!` avoids forming an - // intermediate `&mut` so the close-guard's later borrow stays sound. - file_fd_ptr: Some(unsafe { &mut *ptr::addr_of_mut!(input_file_fd) }), + file_fd_ptr: Some(&input_file_fd), file_hash: Some(hash), macro_remappings, macro_js_ctx: transpiler::default_macro_js_value(), @@ -851,10 +846,7 @@ impl TranspilerJob { && is_main && set_break_point_on_first_line(), runtime_transpiler_cache: if !JscRuntimeTranspilerCache::is_disabled() { - // SAFETY: `cache` is a stack local declared above and outlives - // `parse_options`; `addr_of_mut!` avoids an intermediate `&mut` - // so the post-parse `cache.entry.take()` reborrow stays sound. - Some(unsafe { &mut *ptr::addr_of_mut!(cache) }) + Some(&mut cache) } else { None }, @@ -868,18 +860,11 @@ impl TranspilerJob { // `defer { if should_close && input_file_fd.isValid() { close } }` let _close_fd_guard = scopeguard::guard( - ( - &should_close_input_file_fd, - ptr::addr_of_mut!(input_file_fd), - ), - |(should, fd_ptr)| { - // SAFETY: `input_file_fd` outlives this guard (declared earlier - // in fn scope); no `&mut` alias is live at drop time. - unsafe { - if should.get() && (*fd_ptr).is_valid() { - (*fd_ptr).close(); - *fd_ptr = Fd::INVALID; - } + (&should_close_input_file_fd, &input_file_fd), + |(should, fd)| { + if should.get() && fd.get().is_valid() { + fd.get().close(); + fd.set(Fd::INVALID); } }, ); @@ -912,7 +897,7 @@ impl TranspilerJob { let Some(mut parse_result) = transpiler .parse_maybe_return_file_only_allow_shared_buffer::(parse_options, None) else { - if is_watcher_enabled && input_file_fd.is_valid() { + if is_watcher_enabled && input_file_fd.get().is_valid() { if !is_node_override && bun_paths::is_absolute(path.text) && !strings::contains(path.text, b"node_modules") @@ -923,7 +908,7 @@ impl TranspilerJob { // `&ImportWatcher` is live here, and `add_file` is // thread-safe via watcher mutex. let _ = unsafe { iw.assume_mut() }.add_file::( - input_file_fd, + input_file_fd.get(), path.text, hash, loader, @@ -938,7 +923,7 @@ impl TranspilerJob { return; }; - if is_watcher_enabled && input_file_fd.is_valid() { + if is_watcher_enabled && input_file_fd.get().is_valid() { if !is_node_override && bun_paths::is_absolute(path.text) && !strings::contains(path.text, b"node_modules") @@ -949,7 +934,7 @@ impl TranspilerJob { // `&ImportWatcher` is live here, and `add_file` is // thread-safe via watcher mutex. let _ = unsafe { iw.assume_mut() }.add_file::( - input_file_fd, + input_file_fd.get(), path.text, hash, loader, @@ -1076,17 +1061,14 @@ impl TranspilerJob { let source_code_printer = tls_get_or_leak(&SOURCE_CODE_PRINTER, || { let writer = BufferWriter::init(); - let mut bp = Box::new(BufferPrinter::init(writer)); + let mut bp = BufferPrinter::init(writer); bp.ctx.append_null_byte = false; - bp + Box::new(JsCell::new(bp)) }); // Swap the buffer out and write it back via the // _writeback guard (the thread-local's buffer is reused). - let mut printer = core::mem::replace( - source_code_printer, - BufferPrinter::init(BufferWriter::init()), - ); + let mut printer = source_code_printer.replace(BufferPrinter::init(BufferWriter::init())); printer.ctx.reset(); // Cap buffer size to prevent unbounded growth @@ -1094,12 +1076,9 @@ impl TranspilerJob { if printer.ctx.buffer.list.capacity() > MAX_BUFFER_CAP { // printer.ctx.buffer.deinit() → Drop let writer = BufferWriter::init(); - *source_code_printer = BufferPrinter::init(writer); - source_code_printer.ctx.append_null_byte = false; - printer = core::mem::replace( - source_code_printer, - BufferPrinter::init(BufferWriter::init()), - ); + source_code_printer.set(BufferPrinter::init(writer)); + source_code_printer.with_mut(|p| p.ctx.append_null_byte = false); + printer = source_code_printer.replace(BufferPrinter::init(BufferWriter::init())); } let is_commonjs_module = parse_result.ast.has_commonjs_export_names @@ -1124,12 +1103,6 @@ impl TranspilerJob { if let Some(mi) = module_info.as_deref_mut() { mi.flags.has_tla = !parse_result.ast.top_level_await_keyword.is_empty(); } - // Note: derive `*mut` from a `&mut` borrow (not `&x as *const _ as - // *mut _`, which is Stacked-Borrows UB). The `&mut` borrow ends when the - // closure returns; the raw pointer stays valid until `module_info` is - // moved/touched again (after `print_with_source_map`). - let module_info_ptr: Option<*mut analyze_transpiled_module::ModuleInfo> = - module_info.as_deref_mut().map(std::ptr::from_mut); let print_result = { // SAFETY: see `vm` note above — `from_raw` stores `vm` as a raw @@ -1137,10 +1110,7 @@ impl TranspilerJob { // inside `get()`. No `&mut VirtualMachine` is ever formed. let mut mapper = unsafe { SourceMapHandlerGetter::from_raw(vm, &raw mut printer) }; let _writeback = scopeguard::guard( - ( - std::ptr::from_mut::(source_code_printer), - ptr::addr_of_mut!(printer), - ), + (source_code_printer.as_ptr(), ptr::addr_of_mut!(printer)), |(dst, src)| { // SAFETY: both pointees outlive this scope; no aliases at drop. unsafe { @@ -1157,7 +1127,7 @@ impl TranspilerJob { &mut printer, js_printer::Format::EsmAscii, mapper.get(), - module_info_ptr, + module_info.as_deref_mut(), ) }; if let Err(err) = print_result { @@ -1171,11 +1141,11 @@ impl TranspilerJob { if bun_core::env::DUMP_SOURCE { // SAFETY: `vm` is the live owning VM (BACKREF — see `vm` note above). let vm = unsafe { NonNull::new_unchecked(vm) }; - dump_source(vm, specifier, source_code_printer); + dump_source(vm, specifier, source_code_printer.get()); } let source_code = 'brk: { - let written = source_code_printer.ctx.get_written(); + let written = source_code_printer.get().ctx.get_written(); // The `Jsc` vtable bridge `put()` does not write // `cache.output_code` (only the `r#impl == None` fallback does, @@ -1187,8 +1157,8 @@ impl TranspilerJob { if written.len() > 1024 * 1024 * 2 || unsafe { (*vm).smol } { // printer.ctx.buffer.deinit() → Drop let writer = BufferWriter::init(); - *source_code_printer = BufferPrinter::init(writer); - source_code_printer.ctx.append_null_byte = false; + source_code_printer.set(BufferPrinter::init(writer)); + source_code_printer.with_mut(|p| p.ctx.append_null_byte = false); } // else: writeback guard already restored `printer` into the thread-local. diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index a713132f35f9..225a27c91fd7 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -178,13 +178,13 @@ pub struct VirtualMachine { /// both types are owned by `bun_runtime` (forward dep). Access goes through /// [`RuntimeHooks::timer_insert`] / [`RuntimeHooks::body_value_hive_ref`]. pub runtime_state: *mut c_void, - pub event_loop_handle: Option<*mut PlatformEventLoop>, + pub event_loop_handle: Option>, /// Pending `unref` count drained by the event-loop thread. Atomic because /// `KeepAlive::unref_on_next_tick_concurrently` increments it from OTHER /// threads. pub pending_unref_counter: core::sync::atomic::AtomicI32, pub preload: Vec>, - pub unhandled_pending_rejection_to_capture: Option<*mut JSValue>, + pub unhandled_pending_rejection_to_capture: Option>>, // Note: layering — the concrete `bun_standalone_graph::Graph` lives // in a higher-tier crate. The resolver already broke that cycle with the // `bun_resolver::StandaloneModuleGraph` trait; we hold the same trait @@ -298,7 +298,9 @@ pub struct VirtualMachine { pub on_unhandled_rejection: OnUnhandledRejection, pub on_unhandled_rejection_ctx: Option<*mut c_void>, - pub on_unhandled_rejection_exception_list: Option>, + /// BACKREF — a caller-owned `ExceptionList` stashed for the duration of a + /// single `on_unhandled_rejection` dispatch; restored by the caller. + pub on_unhandled_rejection_exception_list: Option>, pub unhandled_error_counter: usize, pub is_handling_uncaught_exception: bool, pub exit_on_uncaught_exception: bool, @@ -855,14 +857,12 @@ impl VirtualMachine { self.fs().top_level_dir } - /// Safe `&mut Debugger` accessor — the [`JsCell`] escape hatch applied to - /// the optional boxed `Debugger`. Same single-JS-thread soundness - /// contract as [`Self::as_mut`]; keep the borrow short and do not hold - /// across reentrant JS calls. + /// Shared accessor for the optional boxed `Debugger`. Every field the JS + /// thread mutates through this borrow is a `Cell`, so the reference stays + /// valid across reentrant JS. #[inline] - #[allow(clippy::mut_from_ref)] - pub fn debugger_mut(&self) -> Option<&mut crate::debugger::Debugger> { - self.as_mut().debugger.as_deref_mut() + pub fn debugger(&self) -> Option<&crate::debugger::Debugger> { + self.debugger.as_deref() } /// Safe `&mut uws::Loop` accessor for the per-VM uSockets loop. Same @@ -892,7 +892,7 @@ impl VirtualMachine { // `ensure_waker()` to the live per-VM uws/uv loop and remains valid // for the VM lifetime. Single-JS-thread invariant per `unsafe impl // Sync` — only the owning JS thread reborrows mutably. - self.event_loop_handle.map(|h| unsafe { &mut *h }) + self.event_loop_handle.map(|h| unsafe { &mut *h.as_ptr() }) } /// Read-then-zero `pending_unref_counter`. `swap(0)` so a concurrent @@ -1019,7 +1019,7 @@ impl VirtualMachine { ); // SAFETY: set in `init()` on the JS thread before any host_fn / // event-loop tick runs; never cleared while the VM is live. - unsafe { self.event_loop_handle.unwrap_unchecked() } + unsafe { self.event_loop_handle.unwrap_unchecked() }.as_ptr() } #[cfg(not(unix))] { @@ -1076,8 +1076,9 @@ impl VirtualMachine { this.unhandled_error_counter += 1; value.ensure_still_alive(); if let Some(ptr) = this.unhandled_pending_rejection_to_capture { - // SAFETY: caller passed &mut stack_var (see LIFETIMES.tsv) - unsafe { *ptr = value }; + // SAFETY: points at the live `Cell` stack local of the + // frame that installed the capture slot. + unsafe { ptr.as_ref() }.set(value); } } @@ -1104,7 +1105,7 @@ impl VirtualMachine { // SAFETY: BORROW_PARAM ptr set by caller; outlives this call. let list = this .on_unhandled_rejection_exception_list - .map(|mut p| unsafe { p.as_mut() }); + .map(|p| unsafe { &mut *p.as_ptr() }); this.run_error_handler(value, list); } @@ -1822,7 +1823,7 @@ fn vm_from_owner<'a>(owner: *mut ()) -> &'a mut VirtualMachine { bun_io::link_impl_EventLoopCtx! { Js for VirtualMachine => |this| { - platform_event_loop_ptr() => vm_from_owner(this.cast()).uws_loop(), + platform_event_loop_ptr() => bun_ptr::ParentRef::from_raw_mut(vm_from_owner(this.cast()).uws_loop()), file_polls_ptr() => { let rare = vm_from_owner(this.cast()).rare_data(); &raw mut **rare.file_polls_.get_or_insert_with(|| Box::new(bun_io::Store::init())) @@ -3151,11 +3152,12 @@ impl VirtualMachine { { return self .event_loop_handle - .expect("libuv event_loop_handle is null"); + .expect("libuv event_loop_handle is null") + .as_ptr(); } #[cfg(not(debug_assertions))] { - self.event_loop_handle.unwrap() + self.event_loop_handle.unwrap().as_ptr() } } @@ -3593,9 +3595,11 @@ impl VirtualMachine { } /// Enqueues a task from another thread onto this VM's event loop. + /// Takes `&self`: the push is a lock-free intrusive queue op reached via + /// `event_loop_mut()`, and callers are on a foreign thread. #[inline] pub fn enqueue_task_concurrent( - &mut self, + &self, task: core::ptr::NonNull, ) { self.event_loop_mut().enqueue_task_concurrent(task); @@ -5223,14 +5227,6 @@ impl VirtualMachine { enable_source_code_preview: &enable_source_code_preview, source_code_slice, }; - // SAFETY: re-borrow through the guard's raw ptrs; `_tail` does not - // touch them until Drop, so no aliasing during the body. - let exception: &mut ZigException = unsafe { &mut *_tail.exception }; - // SAFETY: as above — re-borrow through the guard's raw ptr; `_tail` - // does not touch `source_code_slice` until Drop. - let source_code_slice: &mut Option = - unsafe { &mut *_tail.source_code_slice.cast_mut() }; - fn is_noisy_builtin(name: &bun_core::String) -> bool { name.eql_comptime("asyncModuleEvaluation") || name.eql_comptime("link") @@ -5578,9 +5574,13 @@ impl VirtualMachine { let exception: *mut ZigException = exception_holder.zig_exception(); let mut source_code_slice: Option = None; + // SAFETY: `exception` points at `exception_holder.zig_exception`; the + // sibling `&mut exception_holder.need_to_clear_parser_arena_on_deinit` + // below covers a disjoint field, so this reborrow stays live across it. + let exception = unsafe { &mut *exception }; + self.remap_zig_exception( - // SAFETY: `exception` points into stack-local `exception_holder`. - unsafe { &mut *exception }, + exception, error_instance, exception_list, &mut exception_holder.need_to_clear_parser_arena_on_deinit, @@ -5590,8 +5590,7 @@ impl VirtualMachine { error_instance.ensure_still_alive(); let result = self.print_error_instance_body( - // SAFETY: see above. - unsafe { &mut *exception }, + exception, error_instance, None, // Note: `exception_list` was already // consumed by `remap_zig_exception` above (only writer). diff --git a/src/jsc/WorkTask.rs b/src/jsc/WorkTask.rs index aa90ad9373a0..ab8b23072439 100644 --- a/src/jsc/WorkTask.rs +++ b/src/jsc/WorkTask.rs @@ -78,19 +78,18 @@ impl WorkTask { // The intrusive `task` field is recovered via container_of in // run_from_thread_pool, so this must live at a stable heap address as a - // raw pointer. Paired with `heap::take` in `destroy`. + // raw pointer. Paired with `heap::take` at the `destroy` call site. bun_core::heap::into_raw(this) } // Not `impl Drop` — `ref_.unref` is also called from `run_from_js`, // and `Self` is held as a raw pointer (intrusive task), so destruction // is explicit. - pub unsafe fn destroy(this: *mut Self) { - // SAFETY: `this` was produced by heap::alloc in create_on_js_thread and - // has not been freed. - let mut this = unsafe { bun_core::heap::take(this) }; - this.ref_.unref(Async::js_vm_ctx()); - // drop(this) — Box freed at scope exit + // `boxed_local`: the `Box` is the point — it is the ownership unit being + // reclaimed, and every owned field drops with it. + #[allow(clippy::boxed_local)] + pub fn destroy(mut self: Box) { + self.ref_.unref(Async::js_vm_ctx()); } pub unsafe fn run_from_thread_pool(task: *mut WorkPoolTask) { @@ -108,12 +107,15 @@ impl WorkTask { Context::run(ctx, this); } - pub fn run_from_js(this: &mut Self) -> Result<(), crate::JsTerminated> { + /// Consumes the task. The keep-alive is dropped and the box freed *before* + /// `Context::then` re-enters JS, so no borrow of `Self` spans the callback. + pub fn run_from_js(this: Box) -> Result<(), crate::JsTerminated> { let ctx = this.ctx; let tracker = this.async_task_tracker; - let global_this = this.global_this.get(); - this.ref_.unref(Async::js_vm_ctx()); + let global_this = this.global_this; + Self::destroy(this); + let global_this = global_this.get(); let _dispatch = tracker.dispatch(global_this); Context::then(ctx, global_this) } diff --git a/src/jsc/any_task_job.rs b/src/jsc/any_task_job.rs index 870232d0fae8..f7e05788dbce 100644 --- a/src/jsc/any_task_job.rs +++ b/src/jsc/any_task_job.rs @@ -22,7 +22,7 @@ use crate::{JSGlobalObject, JsResult, VirtualMachineRef as VirtualMachine}; /// supplied by [`AnyTaskJob`]. /// /// `Drop` on the implementor is the deinit path — it runs on the JS thread -/// (from `run_from_js`'s `heap::take`) on every exit, including the +/// (when `run_from_js`'s `Box` drops) on every exit, including the /// `is_shutting_down` early-out and `init` failure. pub trait AnyTaskJobCtx: Sized { /// Optional fallible JS-thread setup, run after heap allocation but before @@ -85,14 +85,18 @@ impl AnyTaskJob { poll: KeepAlive::default(), ctx, })); - // SAFETY: `job` was just allocated and is exclusively owned here. // Build the erased AnyTask directly with a non-capturing shim. - unsafe { - (*job).any_task = AnyTask { - ctx: NonNull::new(job.cast::()), - callback: |p: *mut c_void| Self::run_from_js(p.cast::()).map_err(Into::into), - }; - } + let any_task = AnyTask { + ctx: NonNull::new(job.cast::()), + callback: |p: *mut c_void| { + // SAFETY: `p` is the `heap::into_raw` allocation below; the + // `AnyTask` fires exactly once, so this is the unique owner. + let this = unsafe { bun_core::heap::take(p.cast::()) }; + Self::run_from_js(this).map_err(Into::into) + }, + }; + // SAFETY: `job` was just allocated and is exclusively owned here. + unsafe { (*job).any_task = any_task }; // `ctx.init` may throw (e.g. CryptoJob); on error, reclaim the // box so `Drop for C` releases any resources `ctx` already owns. let mut guard = scopeguard::guard(job, |job| { @@ -107,23 +111,19 @@ impl AnyTaskJob { /// `KeepAlive::ref_` the JS event loop and hand the intrusive task to the /// work pool. Ownership transfers to the pool → `run_task` → /// `run_from_js`. - /// - /// # Safety - /// `this` must be a live pointer returned by [`Self::create`] that has not - /// yet been scheduled. - pub unsafe fn schedule(this: *mut Self) { - // SAFETY: caller contract. - let this = unsafe { &mut *this }; - this.poll.ref_(bun_io::js_vm_ctx()); - WorkPool::schedule(&raw mut this.task); + pub fn schedule(mut self: Box) { + self.poll.ref_(bun_io::js_vm_ctx()); + let this = bun_core::heap::into_raw(self); + // SAFETY: `this` is the allocation just leaked above; the pool owns it now. + WorkPool::schedule(unsafe { &raw mut (*this).task }); } /// [`Self::create`] + [`Self::schedule`]. For callers that don't need to /// read back from `ctx` after scheduling. pub fn create_and_schedule(global: &JSGlobalObject, ctx: C) -> JsResult<()> { let job = Self::create(global, ctx)?; - // SAFETY: `job` is a freshly-created live pointer. - unsafe { Self::schedule(job) }; + // SAFETY: `job` is a freshly-created, unscheduled, owned allocation. + Self::schedule(unsafe { bun_core::heap::take(job) }); Ok(()) } @@ -147,17 +147,17 @@ impl AnyTaskJob { .enqueue_task_concurrent(ConcurrentTask::create(job.any_task.task())); } - /// `AnyTask` callback — runs ON the JS thread. Reclaims the heap + /// `AnyTask` callback — runs ON the JS thread. Consumes the heap /// allocation; `Drop for Self` (poll.unref) and `Drop for C` run on every /// path. - fn run_from_js(this: *mut Self) -> JsResult<()> { - // SAFETY: `this` was produced by `heap::into_raw` in `create` and is - // uniquely owned here (the `AnyTask` fires exactly once). - let mut this = unsafe { bun_core::heap::take(this) }; - let vm = this.vm; + // `boxed_local`: consuming the `Box` IS the contract — it is the ownership + // unit the task queue handed back. + #[allow(clippy::boxed_local)] + fn run_from_js(mut self: Box) -> JsResult<()> { + let vm = self.vm; if vm.is_shutting_down() { return Ok(()); } - this.ctx.then(vm.global()) + self.ctx.then(vm.global()) } } diff --git a/src/jsc/btjs.rs b/src/jsc/btjs.rs index f884928f8cb9..6bcf63f82b7e 100644 --- a/src/jsc/btjs.rs +++ b/src/jsc/btjs.rs @@ -146,8 +146,7 @@ fn dump_btjs_trace_debug_impl() -> *const c_char { let w = &mut result_writer; let debug_info: &mut SelfInfo = match get_self_debug_info() { - // SAFETY: lazy debug-only singleton; lldb stopped-process, sole `&mut`. - Ok(di) => unsafe { &mut *di }, + Ok(di) => di, Err(err) => { if write!( w, @@ -460,7 +459,7 @@ fn replace_scalar(slice: &mut [u8], from: u8, to: u8) { // ────────────────────────────────────────────────────────────────────────── #[cfg(debug_assertions)] #[inline] -fn get_self_debug_info() -> Result<*mut SelfInfo, Error> { +fn get_self_debug_info() -> Result<&'static mut SelfInfo, Error> { zig_std_debug::get_self_debug_info() } #[cfg(debug_assertions)] diff --git a/src/jsc/event_loop.rs b/src/jsc/event_loop.rs index 5bc9a66c36db..9b421243378f 100644 --- a/src/jsc/event_loop.rs +++ b/src/jsc/event_loop.rs @@ -570,7 +570,9 @@ impl EventLoop { // owning per-thread singleton; non-null and live for the VM lifetime. // `addr_of!` projects to the field place without forming an // intermediate `&VirtualMachine` that would assert no-alias. - unsafe { core::ptr::addr_of!((*vm).event_loop_handle).read() }.expect("event_loop_handle") + unsafe { core::ptr::addr_of!((*vm).event_loop_handle).read() } + .expect("event_loop_handle") + .as_ptr() } pub fn usockets_loop(&self) -> *mut uws::Loop { @@ -586,9 +588,12 @@ impl EventLoop { } #[cfg(not(windows))] { - self.vm_ref().event_loop_handle.expect( - "usockets_loop: event_loop_handle not initialized (call ensure_waker first)", - ) + self.vm_ref() + .event_loop_handle + .expect( + "usockets_loop: event_loop_handle not initialized (call ensure_waker first)", + ) + .as_ptr() } } @@ -869,7 +874,9 @@ impl EventLoop { } let vm = self.vm(); // SAFETY: `vm` is the live owning VM. - unsafe { (*vm).event_loop_handle = Some(Async::Loop::get()) }; + unsafe { + (*vm).event_loop_handle = Some(bun_ptr::BackRef::from_raw(Async::Loop::get())) + }; // Route through raw addr_of to avoid stacked-borrow // aliasing of the embedded field with its parent. // SAFETY: `vm` is the live owning VM; gc_controller is embedded. @@ -1188,7 +1195,7 @@ pub fn get_active_tasks(global_object: &JSGlobalObject, _frame: &CallFrame) -> J #[cfg(windows)] // SAFETY: `Loop::get()` returns the live process-global `uv_loop_t`. let num_polls: i32 = - i32::try_from(unsafe { (*bun_sys::windows::libuv::Loop::get()).active_handles }) + i32::try_from(unsafe { (*bun_sys::windows::libuv::Loop::get()).active_handles.get() }) .expect("int cast"); #[cfg(not(windows))] // SAFETY: uws::Loop::get() returns a live process-global loop. @@ -1404,7 +1411,9 @@ pub(crate) fn __bun_spawn_sync_event_loop_tick_tasks_only(el: *mut ()) { pub(crate) fn __bun_spawn_sync_vm_get_event_loop_handle( vm: *mut (), ) -> bun_event_loop::SpawnSyncEventLoop::VmEventLoopHandle { - vm_from_ptr(vm).event_loop_handle.and_then(NonNull::new) + vm_from_ptr(vm) + .event_loop_handle + .and_then(|h| NonNull::new(h.as_ptr())) } #[unsafe(no_mangle)] @@ -1412,7 +1421,7 @@ pub(crate) fn __bun_spawn_sync_vm_set_event_loop_handle( vm: *mut (), h: bun_event_loop::SpawnSyncEventLoop::VmEventLoopHandle, ) { - vm_from_ptr(vm).event_loop_handle = h.map(NonNull::as_ptr); + vm_from_ptr(vm).event_loop_handle = h.map(bun_ptr::BackRef::from); } #[unsafe(no_mangle)] diff --git a/src/jsc/hot_reloader.rs b/src/jsc/hot_reloader.rs index 568e2613a4b2..76670c46289e 100644 --- a/src/jsc/hot_reloader.rs +++ b/src/jsc/hot_reloader.rs @@ -623,17 +623,6 @@ where self.count += 1; } - /// The dispatched task was heap-allocated in [`Self::enqueue`] via - /// `heap::alloc`; the event loop calls this after `run()` to free it. - /// - /// # Safety - /// `this` must have been created via `heap::alloc` in [`Self::enqueue`] - /// and must not be used after this call. - pub unsafe fn deinit(this: *mut Self) { - // SAFETY: precondition — `this` came from heap::alloc in `enqueue`. - drop(unsafe { bun_core::heap::take(this) }); - } - pub fn run(&mut self) { // Since we rely on the event loop for hot reloads, there can be // a delay before the next reload begins. In the time between the @@ -1202,7 +1191,7 @@ where ent.set_cache_fd(Fd::INVALID); ent.need_stat.set(true); } - path_string = ent.abs_path; + path_string = ent.abs_path(); file_hash = Watcher::get_hash(path_string.as_bytes()); for (entry_id, hash) in hashes.iter().enumerate() { if *hash == file_hash { diff --git a/src/jsc/ipc.rs b/src/jsc/ipc.rs index a9c59e7423e0..8ca20fdaefb9 100644 --- a/src/jsc/ipc.rs +++ b/src/jsc/ipc.rs @@ -1,3 +1,4 @@ +use core::cell::Cell; use core::ffi::{c_int, c_void}; use core::mem::size_of; @@ -5,7 +6,7 @@ use crate as jsc; use crate::js_value::Protected; use crate::json_line_buffer::JSONLineBuffer; use crate::virtual_machine::VirtualMachine; -use crate::{JSGlobalObject, JSValue, JsError, JsResult, SerializedFlags, Task}; +use crate::{JSGlobalObject, JSValue, JsCell, JsError, JsResult, SerializedFlags, Task}; use bun_collections::{ByteVecExt, VecExt}; use bun_core::{Output, handle_oom}; use bun_core::{String as BunString, strings}; @@ -52,39 +53,39 @@ use bun_uws; /// docs/PORTING.md) — `SendQueue` stores one inline so the struct must live at this tier. /// All field accesses + dispatch methods need only `bun_jsc`/`bun_collections` symbols. pub struct InternalMsgHolder { - pub seq: i32, + pub seq: Cell, // TODO: move this to an Array or a JS Object or something which doesn't // individually create a Strong for every single IPC message... - pub callbacks: bun_collections::ArrayHashMap, - pub worker: crate::StrongOptional, - pub cb: crate::StrongOptional, - pub messages: Vec, + pub callbacks: JsCell>, + pub worker: JsCell, + pub cb: JsCell, + pub messages: JsCell>, } impl Default for InternalMsgHolder { fn default() -> Self { Self { - seq: 0, - callbacks: bun_collections::ArrayHashMap::default(), - worker: crate::StrongOptional::empty(), - cb: crate::StrongOptional::empty(), - messages: Vec::new(), + seq: Cell::new(0), + callbacks: JsCell::new(bun_collections::ArrayHashMap::default()), + worker: JsCell::new(crate::StrongOptional::empty()), + cb: JsCell::new(crate::StrongOptional::empty()), + messages: JsCell::new(Vec::new()), } } } impl InternalMsgHolder { pub fn is_ready(&self) -> bool { - self.worker.has() && self.cb.has() + self.worker.get().has() && self.cb.get().has() } - pub fn enqueue(&mut self, message: JSValue, global: &JSGlobalObject) { - self.messages - .push(crate::StrongOptional::create(message, global)); + pub fn enqueue(&self, message: JSValue, global: &JSGlobalObject) { + let strong = crate::StrongOptional::create(message, global); + self.messages.with_mut(|m| m.push(strong)); } - pub fn dispatch(&mut self, message: JSValue, global: &JSGlobalObject) -> JsResult<()> { + pub fn dispatch(&self, message: JSValue, global: &JSGlobalObject) -> JsResult<()> { if !self.is_ready() { self.enqueue(message, global); return Ok(()); @@ -92,9 +93,9 @@ impl InternalMsgHolder { self.dispatch_unsafe(message, global) } - fn dispatch_unsafe(&mut self, message: JSValue, global: &JSGlobalObject) -> JsResult<()> { - let cb = self.cb.get().unwrap(); - let worker = self.worker.get().unwrap(); + fn dispatch_unsafe(&self, message: JSValue, global: &JSGlobalObject) -> JsResult<()> { + let cb = self.cb.get().get().unwrap(); + let worker = self.worker.get().get().unwrap(); let event_loop = global.bun_vm().event_loop_mut(); @@ -103,14 +104,18 @@ impl InternalMsgHolder { let ack = p.to_int32(); // Note: peek the JSValue first (ending the immutable borrow), // then swap_remove (which drops the Strong). - let entry = self.callbacks.get(&ack).map(|s| s.get()); + let entry = self.callbacks.get().get(&ack).map(|s| s.get()); if let Some(callback_opt) = entry { if let Some(callback) = callback_opt { - self.callbacks.swap_remove(&ack); + self.callbacks.with_mut(|c| { + c.swap_remove(&ack); + }); + // Read the worker out of the cell before re-entering JS. + let worker = self.worker.get().get().unwrap(); event_loop.run_callback( callback, global, - self.worker.get().unwrap(), + worker, &[ message, JSValue::NULL, // handle @@ -133,26 +138,15 @@ impl InternalMsgHolder { Ok(()) } - pub fn flush(&mut self, global: &JSGlobalObject) -> JsResult<()> { + pub fn flush(&self, global: &JSGlobalObject) -> JsResult<()> { debug_assert!(self.is_ready()); - // PORT_NOTES_PLAN R-2: `&mut self` carries LLVM `noalias`, but - // `dispatch_unsafe` → `event_loop.run_callback` runs the JS IPC - // listener which can re-enter via a fresh `&mut Self` from the - // owner's `m_ctx` and write `self.cb` / `self.worker` / - // `self.callbacks`. With the loop body inlined, LLVM was hoisting the - // `self.cb`/`self.worker` reads (at the top of `dispatch_unsafe`) out - // of the loop — ASM-verified PROVEN_CACHED. Launder so each iteration - // re-reads through an opaque pointer. - let this: *mut Self = core::hint::black_box(core::ptr::from_mut(self)); - // SAFETY: `this` aliases the live `&mut self`; single JS thread. - let messages = core::mem::take(unsafe { &mut (*this).messages }); + // The `JsCell` fields make `Self` non-`Freeze`, so `&self` is neither + // `noalias` nor `readonly`: `dispatch_unsafe` re-reads `cb`/`worker` + // through the cells on every iteration, even when JS rewrites them. + let messages = self.messages.replace(Vec::new()); for strong in messages { if let Some(message) = strong.get() { - // SAFETY: `this` is still live across re-entry — the IPC - // dispatcher is owned by the Subprocess/Worker which outlives - // this `flush` frame; `&mut *this` is the unique mutable view - // for this call. - unsafe { &mut *this }.dispatch_unsafe(message, global)?; + self.dispatch_unsafe(message, global)?; } // strong drops here (== `strong.deinit()`) } @@ -772,7 +766,9 @@ pub struct WindowsWrite { pub write_req: uv::uv_write_t, pub write_buffer: uv::uv_buf_t, pub write_slice: Box<[u8]>, - pub owner: Option<*mut SendQueue>, + /// BACKREF to the SendQueue that issued the write; `None` once the queue + /// has disconnected (see `_socket_closed`). + pub owner: Option>, } #[cfg(windows)] @@ -888,6 +884,28 @@ pub enum SocketUnion { Closed, } +const TASK_CLOSE_SOCKET: u8 = 0; +const TASK_AFTER_IPC_CLOSED: u8 = 1; + +/// The one site that turns a `ManagedTask` ctx pointer back into +/// `&mut SendQueue`. Each arm scopes that borrow so none of them is live +/// across a call that re-enters JS. +fn send_queue_task(this: *mut SendQueue) -> bun_event_loop::JsResult<()> { + if KIND == TASK_CLOSE_SOCKET { + // SAFETY: `this` is the live `*mut SendQueue` handed to `ManagedTask::new`; + // the task is cancelled in `Drop` before the storage is freed. + unsafe { &mut *this }._close_socket_task(); + } else { + // SAFETY: as above. The reborrow dies with the statement, so no + // `&mut SendQueue` is live once `handle_ipc_close` re-enters JS. + if let Some(owner) = unsafe { &mut *this }._on_after_ipc_closed() { + // SAFETY: BACKREF — owner embeds this SendQueue inline and outlives it. + unsafe { (*owner).handle_ipc_close() }; + } + } + Ok(()) +} + impl SendQueue { /// Safe `&dyn SendQueueOwner` accessor — wraps the per-use raw deref + /// autoref for `&self`-taking trait methods (`kind`, `this_jsvalue`, @@ -999,12 +1017,10 @@ impl SendQueue { // owner is about to free the memory that backs `this`, so scheduling // a task that points back into it would use-after-free. if was_open && self.after_close_task.is_none() { - // Note: `bun_event_loop::JsResult` erases the error to `*mut ()`; - // adapt the jsc-crate `JsResult` via a non-capturing closure (coerces to fn ptr). - let task = ManagedTask::new(std::ptr::from_mut::(self), |p| { - let _ = Self::_on_after_ipc_closed(p); - Ok(()) - }); + let task = ManagedTask::new( + std::ptr::from_mut::(self), + send_queue_task::, + ); self.after_close_task = Some(task); // Do NOT materialize `&mut VirtualMachine` from // `bun_vm()`'s shared `&VirtualMachine` (Stacked-Borrows UB — @@ -1055,11 +1071,10 @@ impl SendQueue { self.close_socket(CloseReason::Normal, CloseFrom::User); return; } - // Note: see `_socket_closed` — adapt `bun_event_loop::JsResult` via closure. - let task = ManagedTask::new(std::ptr::from_mut::(self), |p| { - let _ = Self::_close_socket_task(p); - Ok(()) - }); + let task = ManagedTask::new( + std::ptr::from_mut::(self), + send_queue_task::, + ); self.close_next_tick = Some(task); // SAFETY: VirtualMachine::get() returns the singleton; enqueue_task // only mutates the task queue. @@ -1068,29 +1083,23 @@ impl SendQueue { .enqueue_task(self.close_next_tick.unwrap()); } - fn _close_socket_task(this: *mut SendQueue) -> JsResult<()> { - // SAFETY: `this` was the live `*mut SendQueue` passed to ManagedTask::new; - // the task is cancelled in Drop before the storage is freed. - let this = unsafe { &mut *this }; + fn _close_socket_task(&mut self) { log!("SendQueue#closeSocketTask"); - debug_assert!(this.close_next_tick.is_some()); - this.close_next_tick = None; - this.close_socket(CloseReason::Normal, CloseFrom::User); - Ok(()) + debug_assert!(self.close_next_tick.is_some()); + self.close_next_tick = None; + self.close_socket(CloseReason::Normal, CloseFrom::User); } - fn _on_after_ipc_closed(this: *mut SendQueue) -> JsResult<()> { - // SAFETY: see _close_socket_task. - let this = unsafe { &mut *this }; + /// Returns the owner to notify, so the caller can drop its `&mut self` + /// borrow before `handle_ipc_close` re-enters JS. + fn _on_after_ipc_closed(&mut self) -> Option<*mut dyn SendQueueOwner> { log!("SendQueue#_onAfterIPCClosed"); - this.after_close_task = None; - if this.close_event_sent { - return Ok(()); + self.after_close_task = None; + if self.close_event_sent { + return None; } - this.close_event_sent = true; - // SAFETY: BACKREF — owner embeds this SendQueue inline and outlives it. - unsafe { (*this.owner).handle_ipc_close() }; - Ok(()) + self.close_event_sent = true; + Some(self.owner) } /// returned pointer is invalidated if the queue is modified @@ -1446,7 +1455,9 @@ impl SendQueue { // create write request let mut write_req = Box::new(WindowsWrite { - owner: Some(self as *mut SendQueue), + // SAFETY: reference→raw shares `self`'s tag, so the writes to + // `self` below do not invalidate the stored backref. + owner: Some(unsafe { bun_ptr::BackRef::from_raw(self as *mut SendQueue) }), write_slice: write_req_slice, write_req: bun_core::ffi::zeroed(), write_buffer: uv::uv_buf_t::init(b""), // re-init below after slice address is stable @@ -1526,7 +1537,8 @@ impl SendQueue { // Explicit `&` so the slice `.len()` autoref doesn't trigger // `dangerous_implicit_autorefs` on the raw-ptr place. let write_len = unsafe { (&(*write_req).write_slice).len() }; - let this: *mut SendQueue = 'blk: { + let mut owner: bun_ptr::BackRef = 'blk: { + // SAFETY: libuv handed back the request it was given; still live here. let owner = unsafe { (*write_req).owner }; WindowsWrite::destroy(write_req); match owner { @@ -1534,8 +1546,9 @@ impl SendQueue { None => return, // orelse case if disconnected before the write completes } }; - // SAFETY: owner is a BACKREF into the live SendQueue (cleared in _socket_closed if not). - let this: &mut SendQueue = unsafe { &mut *this }; + // SAFETY: the SendQueue outlives the write request (the backref is cleared + // in `_socket_closed` otherwise); no other borrow of it is live. + let this: &mut SendQueue = unsafe { owner.get_mut() }; let vm = VirtualMachine::get(); // RAII: `enter()` now, `exit()` on drop — replaces the diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index 4008fdefcaae..240f02153fa3 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -1102,10 +1102,8 @@ pub mod resolved_source_tag { } pub use self::resolved_source_tag::ResolvedSourceTag; -// ────────────────────────────────────────────────────────────────────────── -// FetchHeaders — opaque C++ `WebCore::FetchHeaders` handle plus the -// `HTTPHeaderName` enum used by `fast_get`/`fast_has`/`put`. -// ────────────────────────────────────────────────────────────────────────── +// `FetchHeaders` owns one ref on the C++ `WebCore::FetchHeaders`; `HTTPHeaderName` +// is the enum used by `fast_get`/`fast_has`/`put`. #[path = "FetchHeaders.rs"] pub mod fetch_headers; pub use self::fetch_headers::{FetchHeaders, HTTPHeaderName}; diff --git a/src/jsc/rare_data.rs b/src/jsc/rare_data.rs index 2adce5badf0a..93e35d544254 100644 --- a/src/jsc/rare_data.rs +++ b/src/jsc/rare_data.rs @@ -716,16 +716,15 @@ impl RareData { .push(CleanupHook::from(global_this, ctx, func)); } - pub fn spawn_sync_event_loop(&mut self, vm: &mut VirtualMachine) -> &mut SpawnSyncEventLoop { + /// `vm` is only stashed type-erased as `*mut ()`, so it is taken raw: callers + /// need not conjure a second `&mut VirtualMachine` for the duration. + pub fn spawn_sync_event_loop(&mut self, vm: *mut VirtualMachine) -> &mut SpawnSyncEventLoop { if self.spawn_sync_event_loop_.is_none() { // In-place out-param init: `event_loop` inside captures the // `self` address, so the value must not move after init; allocate // the Box first, then init into it. let mut boxed = Box::::new_uninit(); - SpawnSyncEventLoop::init( - &mut *boxed, - core::ptr::from_mut::(vm).cast::<()>(), - ); + SpawnSyncEventLoop::init(&mut *boxed, vm.cast::<()>()); // SAFETY: `init` fully initialised the slot. self.spawn_sync_event_loop_ = Some(unsafe { boxed.assume_init() }); } diff --git a/src/jsc/webcore_types.rs b/src/jsc/webcore_types.rs index 742ccb030cbf..24938e4ba4b8 100644 --- a/src/jsc/webcore_types.rs +++ b/src/jsc/webcore_types.rs @@ -215,9 +215,15 @@ impl Blob { /// Heap-promote and mark as /// heap-allocated so `deinit` knows to free the heap box. #[inline] - pub fn new(mut blob: Blob) -> *mut Blob { + pub fn new(blob: Blob) -> *mut Blob { + bun_core::heap::into_raw(Self::new_boxed(blob)) + } + + /// Same as [`Blob::new`], but ownership stays in the returned `Box`. + #[inline] + pub fn new_boxed(mut blob: Blob) -> Box { blob.ref_count = bun_ptr::RawRefCount::init(1); - bun_core::heap::into_raw(Box::new(blob)) + Box::new(blob) } /// JS-wrapper finalizer (codegen `BlobClass__finalize` thunk). Releases the @@ -519,7 +525,9 @@ pub mod store { #[derive(bun_ptr::ThreadSafeRefCounted)] pub struct Store { pub data: Data, - pub mime_type: MimeType, + /// Written through a shared `&Store` (via `StoreRef: Deref`) on the JS + /// thread; `JsCell` is `#[repr(transparent)]` so layout is unchanged. + pub mime_type: JsCell, pub ref_count: bun_ptr::ThreadSafeRefCount, pub is_all_ascii: Option, } @@ -528,7 +536,7 @@ pub mod store { fn default() -> Self { Self { data: Data::Bytes(Bytes::default()), - mime_type: bun_http_types::MimeType::NONE, + mime_type: JsCell::new(bun_http_types::MimeType::NONE), ref_count: bun_ptr::ThreadSafeRefCount::init(), is_all_ascii: None, } @@ -916,7 +924,7 @@ pub mod store { pub fn init(bytes: Vec) -> StoreRef { StoreRef::from(Store::new(Store { data: Data::Bytes(Bytes::init(bytes)), - mime_type: bun_http_types::MimeType::NONE, + mime_type: JsCell::new(bun_http_types::MimeType::NONE), ref_count: bun_ptr::ThreadSafeRefCount::init(), is_all_ascii: None, })) diff --git a/src/libuv_sys/libuv.rs b/src/libuv_sys/libuv.rs index e1cdaba47f8a..2bf0a0cceb21 100644 --- a/src/libuv_sys/libuv.rs +++ b/src/libuv_sys/libuv.rs @@ -354,7 +354,7 @@ union active_reqs_u { #[repr(C)] pub struct Loop { pub data: *mut c_void, - pub active_handles: c_uint, + pub active_handles: Cell, pub handle_queue: uv__queue, active_reqs: active_reqs_u, pub internal_fields: *mut c_void, @@ -458,42 +458,50 @@ impl Loop { /// (avoid underflow during teardown when Bun's virtual keep-alive refs and /// libuv's own accounting momentarily disagree). #[inline] - pub fn sub_active(&mut self, value: u32) { - log!("subActive({}) - {}", value, self.active_handles); - self.active_handles = self.active_handles.saturating_sub(value); + pub fn sub_active(&self, value: u32) { + log!("subActive({}) - {}", value, self.active_handles.get()); + self.active_handles + .set(self.active_handles.get().saturating_sub(value)); } #[inline] - pub fn add_active(&mut self, value: u32) { + pub fn add_active(&self, value: u32) { log!("addActive({})", value); - self.active_handles = self.active_handles.saturating_add(value); + self.active_handles + .set(self.active_handles.get().saturating_add(value)); } #[inline] - pub fn inc(&mut self) { - log!("inc - {}", self.active_handles.saturating_add(1)); - self.active_handles = self.active_handles.saturating_add(1); + pub fn inc(&self) { + log!("inc - {}", self.active_handles.get().saturating_add(1)); + self.active_handles + .set(self.active_handles.get().saturating_add(1)); } #[inline] - pub fn dec(&mut self) { + pub fn dec(&self) { log!("dec"); - self.active_handles = self.active_handles.saturating_sub(1); + self.active_handles + .set(self.active_handles.get().saturating_sub(1)); } /// `ref`/`unref` aliases for `inc`/`dec`. #[inline] - pub fn ref_(&mut self) { + pub fn ref_(&self) { self.inc(); } #[inline] - pub fn unref(&mut self) { + pub fn unref(&self) { self.dec(); } #[inline] - pub fn unref_count(&mut self, count: i32) { + pub fn unref_count(&self, count: i32) { log!("unrefCount({})", count); // A bare `count as u32` would silently wrap a // negative to ~4 billion and zero out `active_handles`: // assert in debug, clamp in release so we never wrap. debug_assert!(count >= 0, "unref_count: count must be non-negative"); - self.active_handles = self.active_handles.saturating_sub(count.max(0) as u32); + self.active_handles.set( + self.active_handles + .get() + .saturating_sub(count.max(0) as u32), + ); } #[inline] pub fn stop(&mut self) { @@ -731,10 +739,7 @@ pub unsafe trait UvStream: UvHandle { /// `.to_error(Tag::listen)` themselves. #[inline] fn read_start_ctx(&mut self, context: *mut T) -> ReturnCode { - // SAFETY: stream prefix invariant — `&mut Self` reinterprets as - // `&mut Handle` for the leading `UV_HANDLE_FIELDS`. - let h: &mut Handle = unsafe { &mut *(self as *mut Self).cast::() }; - h.data = context.cast(); + self.set_data(context.cast()); unsafe extern "C" fn uv_allocb( req: *mut uv_handle_t, diff --git a/src/opaque/lib.rs b/src/opaque/lib.rs index 46a32c0d6b44..0208c2f88a58 100644 --- a/src/opaque/lib.rs +++ b/src/opaque/lib.rs @@ -428,3 +428,106 @@ pub mod ffi { } } } + +// ── Owned handles to foreign objects ────────────────────────────────────────── + +/// A foreign (C/C++-owned) object whose ownership Rust can hold and give back. +/// +/// Implement via [`foreign_owned!`], never by hand. `release` is the C++ +/// destructor or intrusive-refcount decrement — **not** exclusive access, so it +/// receives `&Self`, matching the `UnsafeCell` in [`opaque_ffi!`]. +/// +/// # Safety +/// `release` must give back exactly one ownership unit (one refcount, or the +/// object), and must be sound to call exactly once per owned handle. +pub unsafe trait ForeignOwned: Sized { + /// # Safety + /// `this` must be live and carry an ownership unit the caller is giving up. + unsafe fn release(this: ::core::ptr::NonNull); +} + +/// Owned handle to a foreign object. +/// +/// `T` : `ForeignRef` :: `Path` : `PathBuf` — the borrowed opaque and its +/// owner. Holds exactly one ownership unit; `Drop` gives it back. +/// +/// There is deliberately **no `DerefMut`**. `T` is an [`opaque_ffi!`] ZST, so +/// `&T` already carries no `noalias`/`readonly` and the foreign owner may mutate +/// through it. A `&mut T` would assert an exclusivity that is never true (C++ +/// holds the object) and never needed (every FFI shim takes `&T`). +/// +/// Not `Clone` — duplicating an ownership unit is a per-type decision. Not +/// `Send`/`Sync` — inherited from `NonNull`. +#[repr(transparent)] +pub struct ForeignRef(::core::ptr::NonNull); + +impl ForeignRef { + /// Adopt an ownership unit the caller is transferring in. + /// + /// # Safety + /// `ptr` must be live and carry exactly one ownership unit that no other + /// handle will give back. + #[inline(always)] + pub const unsafe fn adopt(ptr: ::core::ptr::NonNull) -> Self { + Self(ptr) + } + + #[inline(always)] + pub const fn as_ptr(&self) -> *mut T { + self.0.as_ptr() + } + + #[inline(always)] + pub const fn as_non_null(&self) -> ::core::ptr::NonNull { + self.0 + } + + /// Hand the ownership unit to a foreign owner (a callback context, a C++ + /// field) without giving it back. Pairs with a later [`Self::adopt`]. + #[inline(always)] + pub fn leak(self) -> ::core::ptr::NonNull { + // `ManuallyDrop`, not `mem::forget`: `clippy::mem_forget` is denied here, + // and forgetting a `Drop` type reads as a bug even when it is the point. + ::core::mem::ManuallyDrop::new(self).0 + } +} + +impl ::core::ops::Deref for ForeignRef { + type Target = T; + #[inline(always)] + fn deref(&self) -> &T { + // SAFETY: `self.0` is non-null and live for `self`'s lifetime. + unsafe { opaque_deref_nn(self.0.as_ptr()) } + } +} + +impl Drop for ForeignRef { + #[inline(always)] + fn drop(&mut self) { + // SAFETY: we own exactly one unit; `adopt`/`leak` maintain that. + unsafe { T::release(self.0) } + } +} + +/// Declare that an [`opaque_ffi!`] handle is owned, released by `$release`. +/// +/// `$release` must accept `&$t`: giving back a refcount is not exclusive access. +/// Emits `unsafe impl ForeignOwned`, which is what makes `ForeignRef<$t>` work. +/// +/// ```ignore +/// opaque_ffi! { pub struct FetchHeaders; } +/// foreign_owned!(FetchHeaders, WebCore__FetchHeaders__deref); +/// pub struct FetchHeaders(bun_opaque::ForeignRef); +/// ``` +#[macro_export] +macro_rules! foreign_owned { + ($t:ty, $release:path) => { + // SAFETY: `$release` gives back exactly one ownership unit of `$t`. + unsafe impl $crate::ForeignOwned for $t { + #[inline(always)] + unsafe fn release(this: ::core::ptr::NonNull) { + $release($crate::opaque_deref(this.as_ptr())) + } + } + }; +} diff --git a/src/parsers/toml.rs b/src/parsers/toml.rs index dc1731fac672..904887ac1c9c 100644 --- a/src/parsers/toml.rs +++ b/src/parsers/toml.rs @@ -135,8 +135,7 @@ impl<'a> TOML<'a> { }, next: core::ptr::null_mut(), }); - let head: *mut Rope = rope; - let mut rope: *mut Rope = rope; + let mut cur: *mut Rope = rope; // Hard cap on dotted-key segments. The rope is consumed by `set_rope`, // `get_or_put_array`, and `get_or_put_object`, each of which recurses @@ -156,15 +155,14 @@ impl<'a> TOML<'a> { .add_default_error(b"Dotted key has too many segments")?; return Err(bun_core::err!("SyntaxError")); } - // SAFETY: `rope` points into `bump` and is live for this call; we are - // the sole mutator. Raw pointers used to avoid stacked &mut reborrows. + // SAFETY: `cur` points into `bump` and is live for this call; we are + // the sole mutator. unsafe { - rope = (*rope).append(seg, bump)?; + cur = (*cur).append(seg, bump)?; } } - // SAFETY: `head` was just allocated from `bump` above and is non-null. - Ok(unsafe { &mut *head }) + Ok(rope) } fn run_parser(&mut self) -> Result { diff --git a/src/ptr/weak_ptr.rs b/src/ptr/weak_ptr.rs index acea64e54ce9..4fc936cf1c94 100644 --- a/src/ptr/weak_ptr.rs +++ b/src/ptr/weak_ptr.rs @@ -1,3 +1,4 @@ +use core::cell::Cell; use core::ptr::NonNull; /// Bit layout: @@ -57,13 +58,13 @@ impl Default for WeakPtrData { /// The field projection is a trait method (typically implemented via /// `core::mem::offset_of!`). pub trait HasWeakPtrData { - /// Return a pointer to the embedded `WeakPtrData` field on `this`. + /// Return a pointer to the embedded `WeakPtrData` cell on `this`. /// /// # Safety /// `this` must point to a live allocation of `Self` (the inner contents /// may already be finalized, but the allocation itself must not yet be /// freed). - unsafe fn weak_ptr_data(this: *mut Self) -> *mut WeakPtrData; + unsafe fn weak_ptr_data(this: *mut Self) -> *const Cell; } /// Allow a type to be weakly referenced. This keeps a reference count of how @@ -106,9 +107,11 @@ impl WeakPtr { // SAFETY: caller contract — `this` points to a live `T`. Projecting // straight to the embedded field means no whole-struct `&mut T` is // formed, so `this`'s provenance reaches the stored pointer intact. - let d = unsafe { &mut *T::weak_ptr_data(this) }; - debug_assert!(!d.finalized()); - d.set_reference_count(d.reference_count() + 1); + let d = unsafe { &*T::weak_ptr_data(this) }; + let mut data = d.get(); + debug_assert!(!data.finalized()); + data.set_reference_count(data.reference_count() + 1); + d.set(data); Self { // SAFETY: caller contract — `this` is non-null. raw_ptr: Some(unsafe { NonNull::new_unchecked(this) }), @@ -133,7 +136,7 @@ impl WeakPtr { if let Some(value) = self.raw_ptr { // SAFETY: allocation is live while any WeakPtr holds it (see above). unsafe { - if !(*T::weak_ptr_data(value.as_ptr())).finalized() { + if !(*T::weak_ptr_data(value.as_ptr())).get().finalized() { return Some(&mut *value.as_ptr()); } self.deref_internal(value); @@ -147,17 +150,21 @@ impl WeakPtr { /// allocation whose embedded `WeakPtrData` has `reference_count > 0`. unsafe fn deref_internal(&mut self, value: NonNull) { self.raw_ptr = None; - // SAFETY: caller guarantees `value` points to a live allocation; - // projecting to the embedded `WeakPtrData` field. - let weak_data = unsafe { &mut *T::weak_ptr_data(value.as_ptr()) }; - let count = weak_data.reference_count() - 1; - weak_data.set_reference_count(count); - let finalized = weak_data.finalized(); + let (count, finalized) = { + // SAFETY: caller guarantees `value` points to a live allocation; + // projecting to the embedded `WeakPtrData` field. + let weak_data = unsafe { &*T::weak_ptr_data(value.as_ptr()) }; + let mut data = weak_data.get(); + let count = data.reference_count() - 1; + data.set_reference_count(count); + weak_data.set(data); + (count, data.finalized()) + }; if finalized && count == 0 { // The allocation came from `heap::alloc` (via `Box::new`). // SAFETY: this is the last reference and the owner has finalized, - // so we hold the only pointer to a `Box`-allocated `T`. `weak_data` - // is dead here, so freeing through `value` disturbs no live borrow. + // so we hold the only pointer to a `Box`-allocated `T`. The cell + // borrow ended above, so freeing through `value` disturbs nothing. drop(unsafe { bun_core::heap::take(value.as_ptr()) }); } } @@ -190,7 +197,7 @@ mod tests { } struct Owner { - weak: WeakPtrData, + weak: Cell, /// Inline (not behind a `Box`) so writing it is a write into the /// `Owner` allocation itself — the access a stale handle trips on. payload: u32, @@ -205,15 +212,25 @@ mod tests { } impl HasWeakPtrData for Owner { - unsafe fn weak_ptr_data(this: *mut Self) -> *mut WeakPtrData { + unsafe fn weak_ptr_data(this: *mut Self) -> *const Cell { // SAFETY: caller contract — pure field projection, no read. - unsafe { &raw mut (*this).weak } + unsafe { &raw const (*this).weak } } } + /// `on_finalize` through the cell; true when no weak refs remain. + fn on_finalize(raw: *mut Owner) -> bool { + // SAFETY: `raw` is live. + let cell = unsafe { &*Owner::weak_ptr_data(raw) }; + let mut d = cell.get(); + let last = d.on_finalize(); + cell.set(d); + last + } + fn new_owner(payload: u32) -> *mut Owner { bun_core::heap::into_raw(Box::new(Owner { - weak: WeakPtrData::EMPTY, + weak: Cell::new(WeakPtrData::EMPTY), payload, _heap: Box::new(payload), })) @@ -263,8 +280,7 @@ mod tests { assert_eq!(weak.get().map(|o| o.payload), Some(4)); // Owner finalizes its contents: not the last ref, so the allocation stays. - // SAFETY: `raw` is live. - assert!(!unsafe { (*Owner::weak_ptr_data(raw)).on_finalize() }); + assert!(!on_finalize(raw)); assert_eq!(drops(), before); // `get` on a finalized owner releases the ref and reports `None`, which @@ -329,12 +345,14 @@ mod tests { // SAFETY: see above. let mut b = unsafe { WeakPtr::init_ref(raw) }; // SAFETY: `raw` is live. - assert_eq!(unsafe { (*Owner::weak_ptr_data(raw)).reference_count() }, 2); + assert_eq!( + unsafe { (*Owner::weak_ptr_data(raw)).get().reference_count() }, + 2 + ); assert_eq!(a.get().map(|o| o.payload), Some(2)); assert_eq!(b.get().map(|o| o.payload), Some(2)); - // SAFETY: `raw` is live. - assert!(!unsafe { (*Owner::weak_ptr_data(raw)).on_finalize() }); + assert!(!on_finalize(raw)); a.deref(); assert_eq!(drops(), before); b.deref(); diff --git a/src/resolver/fs.rs b/src/resolver/fs.rs index 02f04b00697d..5ef84433285d 100644 --- a/src/resolver/fs.rs +++ b/src/resolver/fs.rs @@ -12,7 +12,7 @@ use bun_core::{FeatureFlags, Generation, ZStr, env_var}; use bun_paths::resolve_path::platform; use bun_paths::strings; use bun_paths::{MAX_PATH_BYTES, PathBuffer, SEP, resolve_path as path_handler}; -use bun_ptr::Interned; +use bun_ptr::{Interned, ParentRef}; use bun_sys::{self, Fd}; use bun_threading::Mutex; @@ -227,7 +227,7 @@ impl FileSystem { /// in `lib.rs` impl this by forwarding to their own `RealFS::kind`. pub trait EntryKindResolver { fn resolve_kind( - &mut self, + &self, dir: &[u8], base: &[u8], existing_fd: Fd, @@ -269,7 +269,9 @@ impl Default for EntryCache { // below opts back in under that external-locking discipline). pub struct Entry { pub cache: core::cell::Cell, - pub dir: &'static [u8], + /// `Cell` so a shared `&Entry` (an EntryStore slot) can be re-pointed at + /// its directory on a cache refresh; serialized by the per-entry `mutex`. + pub dir: core::cell::Cell<&'static [u8]>, pub base_: strings::StringOrTinyString, @@ -279,7 +281,7 @@ pub struct Entry { pub mutex: Mutex, pub need_stat: core::cell::Cell, - pub abs_path: Interned, + pub abs_path: core::cell::Cell, } impl Entry { @@ -333,47 +335,36 @@ impl Entry { /// Interned in DirnameStore. #[inline] pub fn dir(&self) -> &'static [u8] { - self.dir + self.dir.get() } /// `Interned` is `Copy`. #[inline] pub fn abs_path(&self) -> Interned { - self.abs_path + self.abs_path.get() } #[inline] - pub fn set_abs_path(&mut self, p: Interned) { - self.abs_path = p; + pub fn set_abs_path(&self, p: Interned) { + self.abs_path.set(p); } /// Stat-on-first-use. - /// - /// # Safety - /// `fs` must point to a live `EntryKindResolver` (the process-global - /// `RealFS` singleton in practice). `resolve_kind` must not re-enter - /// this entry's `mutex` (it only performs syscalls and string interning). // `Entry` lives in the EntryStore BSSMap singleton. The lazy-stat rewrite // of `need_stat` / `cache` is serialized on the per-entry `mutex` here - // (double-checked: the cached fast path stays lock-free). `fs` is `*mut` - // so the call site does not require a second exclusive `&mut RealFS` - // borrow while a `&mut Entry` (borrowed out of `RealFS.entries`) is live. + // (double-checked: the cached fast path stays lock-free). // Generic over `R: EntryKindResolver` so this block is independent of // which `RealFS` copy `fs` points at (see file-top comment). - pub unsafe fn kind(&self, fs: *mut R, store_fd: bool) -> EntryKind { + pub fn kind(&self, fs: ParentRef, store_fd: bool) -> EntryKind { if self.need_stat.get() { let _guard = self.mutex.lock_guard(); if self.need_stat.get() { self.need_stat.set(false); // This is technically incorrect, but we are choosing not to handle errors here - // SAFETY: `fs` points at the process-global RealFS singleton; `resolve_kind` - // only does syscalls + string interning, so the short `&mut` cannot alias. - match unsafe { &mut *fs }.resolve_kind( - self.dir, - self.base(), - self.cache().fd, - store_fd, - ) { + match fs + .get() + .resolve_kind(self.dir.get(), self.base(), self.cache().fd, store_fd) + { Ok(c) => self.cache.set(c), Err(_) => return self.cache().kind, } @@ -382,28 +373,18 @@ impl Entry { self.cache().kind } - /// - /// # Safety - /// `fs` must point to a live `EntryKindResolver` (the process-global - /// `RealFS` singleton in practice). See [`Entry::kind`]. - pub unsafe fn symlink( - &self, - fs: *mut R, - store_fd: bool, - ) -> &'static [u8] { + /// Stat-on-first-use; see [`Entry::kind`]. + pub fn symlink(&self, fs: ParentRef, store_fd: bool) -> &'static [u8] { if self.need_stat.get() { let _guard = self.mutex.lock_guard(); if self.need_stat.get() { self.need_stat.set(false); // This error can happen if the file was deleted between the time the directory // was scanned and the time it was read - // SAFETY: see the note on `Entry::kind`. - match unsafe { &mut *fs }.resolve_kind( - self.dir, - self.base(), - self.cache().fd, - store_fd, - ) { + match fs + .get() + .resolve_kind(self.dir.get(), self.base(), self.cache().fd, store_fd) + { Ok(c) => self.cache.set(c), Err(_) => return b"", } @@ -421,12 +402,12 @@ impl Clone for Entry { fn clone(&self) -> Self { Self { cache: core::cell::Cell::new(self.cache.get()), - dir: self.dir, + dir: core::cell::Cell::new(self.dir.get()), base_: strings::StringOrTinyString::init(self.base_.slice()), base_lowercase_: strings::StringOrTinyString::init(self.base_lowercase_.slice()), mutex: Mutex::default(), need_stat: core::cell::Cell::new(self.need_stat.get()), - abs_path: self.abs_path, + abs_path: core::cell::Cell::new(self.abs_path.get()), } } } @@ -435,12 +416,12 @@ impl Default for Entry { fn default() -> Self { Self { cache: core::cell::Cell::new(EntryCache::default()), - dir: b"", + dir: core::cell::Cell::new(b"" as &'static [u8]), base_: strings::StringOrTinyString::init(b""), base_lowercase_: strings::StringOrTinyString::init(b""), mutex: Mutex::default(), need_stat: core::cell::Cell::new(true), - abs_path: Interned::EMPTY, + abs_path: core::cell::Cell::new(Interned::EMPTY), } } } @@ -541,18 +522,18 @@ pub mod dir_entry { /// Per-entry hook invoked by `add_entry`/`readdir`. pub trait DirEntryIterator { const IS_VOID: bool = false; - fn next(&self, entry: &mut Entry, fd: Fd); + fn next(&self, entry: &Entry, fd: Fd); } impl DirEntryIterator for () { const IS_VOID: bool = true; - fn next(&self, _entry: &mut Entry, _fd: Fd) {} + fn next(&self, _entry: &Entry, _fd: Fd) {} } impl DirEntryIterator for &T { const IS_VOID: bool = T::IS_VOID; #[inline] - fn next(&self, entry: &mut Entry, fd: Fd) { + fn next(&self, entry: &Entry, fd: Fd) { (**self).next(entry, fd) } } @@ -657,14 +638,14 @@ impl DirEntry { // `name_hash` instead of re-hashing. if let Some(&existing_ptr) = map.get_hashed(name_hash, name_lc) { // SAFETY: EntryStore-owned pointer, valid for lifetime of store - let existing = unsafe { &mut *existing_ptr }; + let existing = unsafe { &*existing_ptr }; // `MutexGuard` stores a `BackRef` (lifetime-erased), so - // holding it does not borrow `existing` — the field writes + // holding it does not borrow `existing` — the `Cell` writes // below remain unconstrained. Replaces the manual // `lock()` + `scopeguard(addr_of!(mutex), |m| (*m).unlock())` // backref-deref pair. let _guard = existing.mutex.lock_guard(); - existing.dir = self.dir; + existing.dir.set(self.dir); existing.need_stat.set( existing.need_stat.get() @@ -718,7 +699,7 @@ impl DirEntry { strings::StringOrTinyString::init_append_if_needed(name_lc, filename_store)? }; addr_of_mut!((*p).base_lowercase_).write(base_lowercase); - addr_of_mut!((*p).dir).write(self.dir); + addr_of_mut!((*p).dir).write(core::cell::Cell::new(self.dir)); addr_of_mut!((*p).mutex).write(Mutex::new()); // Call "stat" lazily for performance. The "@material-ui/icons" package // contains a directory with over 11,000 entries in it and running "stat" @@ -731,13 +712,13 @@ impl DirEntry { kind: found_kind.unwrap_or(EntryKind::File), fd: Fd::INVALID, })); - addr_of_mut!((*p).abs_path).write(Interned::EMPTY); + addr_of_mut!((*p).abs_path).write(core::cell::Cell::new(Interned::EMPTY)); p } }; // SAFETY: just produced from EntryStore append or prev_map lookup - let stored_ref = unsafe { &mut *stored }; + let stored_ref = unsafe { &*stored }; // PERF: the // generic `put` here would heap-box a second key copy. `base_lowercase` @@ -760,7 +741,6 @@ impl DirEntry { } if FeatureFlags::VERBOSE_FS { - // re-borrow `base()` after the `iterator.next` mutable borrow ends. let stored_name = stored_ref.base(); if found_kind == Some(EntryKind::Dir) { bun_core::prettyln!(" + {}/", BStr::new(stored_name)); @@ -933,42 +913,48 @@ pub(crate) struct EntriesGuard { impl EntriesGuard { /// Single `unsafe` deref site for the `entries_option_map()` singleton. /// - /// Private: `&self → &mut Map` is sound only because `self._lock` is the - /// proof-of-exclusivity (`entries_mutex` held), the map lives in a - /// disjoint static allocation, and every caller below uses the borrow - /// for one map operation then drops it — no two `&mut` overlap. Do NOT - /// expose publicly (would let safe code create aliased `&mut`). + /// `&mut self` is the compiler-enforced proof that no two `&mut Map` + /// overlap; `self._lock` (`entries_mutex` held) is the proof no other + /// thread holds one. #[inline] - #[allow(clippy::mut_from_ref)] - fn map_mut(&self) -> &mut EntriesOptionMap { + fn map_mut(&mut self) -> &mut EntriesOptionMap { // SAFETY: `self._lock` holds `entries_mutex` for this guard's whole // lifetime — sole `&mut` to the process-static singleton. The returned - // borrow is tied to `&self` (the guard), so it cannot outlive the lock. + // borrow is tied to `&mut self`, so it cannot outlive the lock. unsafe { &mut *entries_option_map() } } pub(crate) fn get_or_put( - &self, + &mut self, key: &[u8], ) -> core::result::Result { self.map_mut().get_or_put(key) } - pub(crate) fn at_index(&self, index: allocators::IndexType) -> Option<*mut EntriesOption> { + pub(crate) fn at_index(&mut self, index: allocators::IndexType) -> Option<*mut EntriesOption> { let r = self.map_mut().at_index(index)?; Some(std::ptr::from_mut::(r)) } + /// Exclusive borrow of one slot, tied to `&mut self` (the held lock). + /// Callers that need the slot across a `&mut RealFS` call re-borrow by + /// `index` rather than laundering a `*mut EntriesOption`. + pub(crate) fn at_index_mut( + &mut self, + index: allocators::IndexType, + ) -> Option<&mut EntriesOption> { + self.map_mut().at_index(index) + } pub(crate) fn put( - &self, + &mut self, result: &mut allocators::Result, value: EntriesOption, ) -> core::result::Result<*mut EntriesOption, AllocError> { let r = self.map_mut().put(result, value)?; Ok(std::ptr::from_mut::(r)) } - pub(crate) fn mark_not_found(&self, result: allocators::Result) { + pub(crate) fn mark_not_found(&mut self, result: allocators::Result) { self.map_mut().mark_not_found(result) } - pub(crate) fn remove(&self, key: &[u8]) -> bool { + pub(crate) fn remove(&mut self, key: &[u8]) -> bool { self.map_mut().remove(key) } } @@ -1128,65 +1114,56 @@ impl RealFS { // `entries_mutex` for the whole operation so the `&mut BSSMapInner` is // exclusive; it does not borrow `self`, so the `&mut self` calls below // (`readdir`, `read_directory_error`) remain unconstrained while held. - let map = self.entries_locked(); - - // `at_index` returns a raw `*mut EntriesOption`. - // Form short-lived `&mut` only at each use site below; - // never hand a `&'static mut` back to the caller. - let existing_ptr = map.at_index(index)?; - // SAFETY: `entries_mutex` held; no other `&mut` to this slot in scope. - if let EntriesOption::Entries(entries) = unsafe { &mut *existing_ptr } { - if entries.generation < generation { - let dir_path = entries.dir; - // capture raw ptrs to the in-place `DirEntry` fields, then - // drop the short-lived `&mut` before re-borrowing `self` for - // `readdir` / `read_directory_error`. - let entries_ptr: *mut DirEntry = &raw mut **entries; - // SAFETY: derive `prev_map_ptr` FROM `entries_ptr` so both raw ptrs - // share one provenance root. Writing `&mut entries.data` here would - // call `Box::deref_mut` a second time, which under Stacked Borrows - // retags the whole `DirEntry` and invalidates `entries_ptr` — making - // the later `*entries_ptr = new_entry` UB. - let prev_map_ptr: *mut dir_entry::EntryMap = - unsafe { core::ptr::addr_of_mut!((*entries_ptr).data) }; - let handle_dir = match bun_sys::Dir::open(dir_path) { - Ok(h) => h, - Err(err) => { - // SAFETY: `entries_mutex` held; sole access to this slot. - unsafe { (*prev_map_ptr).clear() }; - return Some( - self.read_directory_error(Some(&map), dir_path, err.into()) - .expect("unreachable"), - ); + let mut map = self.entries_locked(); + + // Carry the slab `index`, not a raw slot pointer: each step below + // re-borrows the slot through the guard, so no borrow spans a + // `&mut self` call (`readdir` / `read_directory_error`). + let stale_dir: Option<&'static [u8]> = match map.at_index_mut(index)? { + EntriesOption::Entries(entries) if entries.generation < generation => Some(entries.dir), + _ => None, + }; + if let Some(dir_path) = stale_dir { + let handle_dir = match bun_sys::Dir::open(dir_path) { + Ok(h) => h, + Err(err) => { + if let Some(EntriesOption::Entries(entries)) = map.at_index_mut(index) { + entries.data.clear(); } - }; - let new_entry = match self.readdir( - false, - // `handle_dir` drops at end of this block; never publish. - false, - // SAFETY: `entries_mutex` held; `readdir` does not touch - // `self.entries`, so this `&mut EntryMap` is unaliased. - Some(unsafe { &mut *prev_map_ptr }), - dir_path, - generation, - &handle_dir, - (), - ) { - Ok(e) => e, - Err(err) => { - // SAFETY: see above. - unsafe { (*prev_map_ptr).clear() }; - return Some( - self.read_directory_error(Some(&map), dir_path, err) - .expect("unreachable"), - ); + return Some( + self.read_directory_error(Some(&mut map), dir_path, err.into()) + .expect("unreachable"), + ); + } + }; + let prev_map = match map.at_index_mut(index) { + Some(EntriesOption::Entries(entries)) => Some(&mut entries.data), + _ => None, + }; + let new_entry = match self.readdir( + false, + // `handle_dir` drops at end of this block; never publish. + false, + prev_map, + dir_path, + generation, + &handle_dir, + (), + ) { + Ok(e) => e, + Err(err) => { + if let Some(EntriesOption::Entries(entries)) = map.at_index_mut(index) { + entries.data.clear(); } - }; - // SAFETY: `entries_mutex` held; sole access to this slot. - unsafe { - (*prev_map_ptr).clear(); - *entries_ptr = new_entry; + return Some( + self.read_directory_error(Some(&mut map), dir_path, err) + .expect("unreachable"), + ); } + }; + if let Some(EntriesOption::Entries(entries)) = map.at_index_mut(index) { + entries.data.clear(); + **entries = new_entry; } } @@ -1528,7 +1505,7 @@ impl RealFS { fn read_directory_error( &mut self, - entries: Option<&EntriesGuard>, + entries: Option<&mut EntriesGuard>, dir: &[u8], err: bun_core::Error, ) -> Result<*mut EntriesOption, AllocError> { @@ -1614,7 +1591,7 @@ impl RealFS { // mutex by raw pointer (no borrow of `self`), so the `&mut self` calls below // (`open_dir`, `readdir`, `read_directory_error`) remain unconstrained while // the lock is held. - let entries_guard = if FeatureFlags::ENABLE_ENTRY_CACHE { + let mut entries_guard = if FeatureFlags::ENABLE_ENTRY_CACHE { Some(self.entries_locked()) } else { None @@ -1622,32 +1599,38 @@ impl RealFS { let mut in_place: Option<*mut DirEntry> = None; - if let Some(entries) = entries_guard.as_ref() { + if let Some(entries) = entries_guard.as_mut() { cache_result = Some(entries.get_or_put(dir)?); let cr = cache_result.as_ref().unwrap(); if cr.has_checked_if_exists() { - if let Some(cached_result) = entries.at_index(cr.index) { - // SAFETY: `entries_mutex` held; form a short-lived `&mut` for the - // match only — the raw `*mut` is what escapes to the caller. - match unsafe { &mut *cached_result } { - EntriesOption::Err(_) => return Ok(cached_result), - EntriesOption::Entries(e) if e.generation >= generation => { - return Ok(cached_result); - } - EntriesOption::Entries(e) => { - in_place = Some(&raw mut **e); + // Borrow the slot through the guard; on the cache-hit path + // re-derive the raw handle from `cr.index` for the return, so + // no raw pointer escapes a `&mut EntriesOption`. + let mut cached_hit = false; + match entries.at_index_mut(cr.index) { + Some(EntriesOption::Err(_)) => cached_hit = true, + Some(EntriesOption::Entries(e)) if e.generation >= generation => { + cached_hit = true; + } + Some(EntriesOption::Entries(e)) => { + in_place = Some(&raw mut **e); + } + None => { + if cr.status == allocators::ItemStatus::NotFound && generation == 0 { + return Ok(TEMP_ENTRIES_OPTION.with_borrow_mut(|slot| { + slot.write(EntriesOption::Err(dir_entry::Err { + original_err: bun_core::err!("ENOENT"), + canonical_error: bun_core::err!("ENOENT"), + })); + // threadlocal storage outlives caller; return raw `*mut`. + slot.as_mut_ptr() + })); } } - } else if cr.status == allocators::ItemStatus::NotFound && generation == 0 { - return Ok(TEMP_ENTRIES_OPTION.with_borrow_mut(|slot| { - slot.write(EntriesOption::Err(dir_entry::Err { - original_err: bun_core::err!("ENOENT"), - canonical_error: bun_core::err!("ENOENT"), - })); - // threadlocal storage outlives caller; return raw `*mut`. - slot.as_mut_ptr() - })); + } + if cached_hit { + return Ok(entries.at_index(cr.index).expect("cache slot")); } } } @@ -1662,7 +1645,7 @@ impl RealFS { _opened.as_ref().unwrap() } Err(err) => { - return Ok(self.read_directory_error(entries_guard.as_ref(), dir, err)?); + return Ok(self.read_directory_error(entries_guard.as_mut(), dir, err)?); } }, }; @@ -1697,7 +1680,7 @@ impl RealFS { // SAFETY: see above unsafe { (*existing).data.clear() }; } - return Ok(self.read_directory_error(entries_guard.as_ref(), dir, err)?); + return Ok(self.read_directory_error(entries_guard.as_mut(), dir, err)?); } }; @@ -1712,7 +1695,7 @@ impl RealFS { } } - if let Some(map) = entries_guard.as_ref() { + if let Some(map) = entries_guard.as_mut() { if publish_fd && !entries.fd.is_valid() { entries.fd = handle_fd; } @@ -2155,7 +2138,7 @@ pub fn read_file_with_handle_impl<'p, 'buf, const USE_SHARED_BUFFER: bool, const impl RealFS { pub fn kind( - &mut self, + &self, dir_: &[u8], base: &[u8], existing_fd: Fd, @@ -2343,7 +2326,7 @@ impl RealFS { impl EntryKindResolver for RealFS { #[inline(always)] fn resolve_kind( - &mut self, + &self, dir: &[u8], base: &[u8], existing_fd: Fd, diff --git a/src/resolver/lib.rs b/src/resolver/lib.rs index b7614dca3651..ee6a903b57d7 100644 --- a/src/resolver/lib.rs +++ b/src/resolver/lib.rs @@ -1027,13 +1027,12 @@ pub mod fs { &mut self, result: &mut bun_alloc::Result, value: EntriesOption, - ) -> core::result::Result<*mut EntriesOption, bun_core::Error> { + ) -> core::result::Result<&mut EntriesOption, bun_core::Error> { // `BSSMapInner::put` mutates `result.index` to record placement; callers // (e.g. `dir_info_cached_maybe_log`) re-read `result.index` post-`put`, so the // mutation must be visible — pass through directly. self.inner() .put(result, value) - .map(std::ptr::from_mut::) .map_err(|_| bun_core::err!("OutOfMemory")) } pub fn mark_not_found(&mut self, result: bun_alloc::Result) { @@ -1146,7 +1145,6 @@ pub mod fs { /// Iterate `handle` and populate a /// fresh `DirEntry` (re-using `prev_map` Entry slots where the name matches). fn readdir( - &mut self, store_fd: bool, mut prev_map: Option<&mut dir_entry::EntryMap>, dir_: &'static [u8], @@ -1184,7 +1182,7 @@ pub mod fs { &mut self, dir: &[u8], err: bun_core::Error, - ) -> core::result::Result<&'static mut EntriesOption, bun_core::Error> { + ) -> core::result::Result<&mut EntriesOption, bun_core::Error> { if bun_core::FeatureFlags::ENABLE_ENTRY_CACHE { let mut get_or_put_result = self.entries.get_or_put(dir)?; if err == bun_core::err!("ENOENT") || err == bun_core::err!("FileNotFound") { @@ -1203,8 +1201,7 @@ pub mod fs { canonical_error: err, }), )?; - // SAFETY: BSSMap-owned slot; outlives caller (process-static singleton). - return Ok(unsafe { &mut *opt }); + return Ok(opt); } } @@ -1243,7 +1240,7 @@ pub mod fs { generation: Generation, store_fd: bool, iterator: I, - ) -> core::result::Result<&'static mut EntriesOption, bun_core::Error> { + ) -> core::result::Result<&mut EntriesOption, bun_core::Error> { let dir = strings::paths::without_trailing_slash_windows_path(dir_maybe_trail_slash); crate::Resolver::assert_valid_cache_key(dir); @@ -1261,27 +1258,27 @@ pub mod fs { let cr = cache_result.as_ref().unwrap(); if cr.has_checked_if_exists() { - if let Some(cached_result) = self.entries.at_index(cr.index) { - // erase to raw immediately so the early-return reborrow - // doesn't conflict with the `&mut self.entries` borrow above. - let cached_ptr = std::ptr::from_mut::(cached_result); - // SAFETY: BSSMap-owned slot; uniquely held under `entries_mutex`. - // Single `&mut` reborrow — the catch-all arm binds and returns the - // scrutinee directly so no second `&mut *cached_ptr` is materialized - // while the first is on the borrow stack (Stacked Borrows hygiene). - match unsafe { &mut *cached_ptr } { - EntriesOption::Entries(e) if e.generation < generation => { - in_place = Some(std::ptr::from_mut::(*e)); + let mut cached_index: Option = None; + match self.entries.at_index(cr.index) { + Some(EntriesOption::Entries(e)) if e.generation < generation => { + in_place = Some(std::ptr::from_mut::(*e)); + } + Some(_) => cached_index = Some(cr.index), + None => { + if cr.status == bun_alloc::ItemStatus::NotFound && generation == 0 { + return Ok(temp_entries_option_write(EntriesOption::Err( + dir_entry::Err { + original_err: bun_core::err!("ENOENT"), + canonical_error: bun_core::err!("ENOENT"), + }, + ))); } - cached => return Ok(cached), } - } else if cr.status == bun_alloc::ItemStatus::NotFound && generation == 0 { - return Ok(temp_entries_option_write(EntriesOption::Err( - dir_entry::Err { - original_err: bun_core::err!("ENOENT"), - canonical_error: bun_core::err!("ENOENT"), - }, - ))); + } + // Hand the hit back by slot index: returning the `&mut` bound + // above would keep `self` borrowed over the readdir path below. + if let Some(index) = cached_index { + return Ok(self.entries.at_index(index).expect("cached slot")); } } } @@ -1325,7 +1322,7 @@ pub mod fs { // SAFETY: BSSMap-owned, no aliasing here (entries_mutex held). unsafe { &mut (*p).data } }); - let mut entries = match self.readdir(store_fd, prev, dir, generation, handle, iterator) + let mut entries = match Self::readdir(store_fd, prev, dir, generation, handle, iterator) { Ok(e) => e, Err(err) => { @@ -1362,8 +1359,7 @@ pub mod fs { ); let out = self.entries.put(cache_result.as_mut().unwrap(), result)?; - // SAFETY: BSSMap-owned slot; outlives caller (process-static singleton). - return Ok(unsafe { &mut *out }); + return Ok(out); } // ENABLE_ENTRY_CACHE = false: stash in the threadlocal and hand back its @@ -1394,7 +1390,7 @@ pub mod fs { /// (if reparse point) `CreateFileW`-follow + `GetFinalPathNameByHandle` /// realpath. pub fn kind( - &mut self, + &self, dir_: &[u8], base: &[u8], existing_fd: Fd, @@ -1575,7 +1571,7 @@ pub mod fs { impl crate::fs_full::EntryKindResolver for RealFS { #[inline(always)] fn resolve_kind( - &mut self, + &self, dir: &[u8], base: &[u8], existing_fd: bun_sys::Fd, @@ -1618,52 +1614,55 @@ pub mod fs { index: bun_alloc::IndexType, generation: Generation, ) -> Option<&mut EntriesOption> { - // erase to raw immediately so re-borrowing `&mut self` for - // `open_dir`/`readdir`/`read_directory_error` doesn't conflict. - // `entries_mutex` held by caller; sole `&mut` to this slot. - let result_ptr = std::ptr::from_mut::(self.entries.at_index(index)?); - // SAFETY: BSSMap-owned slot; uniquely held under `entries_mutex`. - if let EntriesOption::Entries(existing) = unsafe { &mut *result_ptr } { - if existing.generation < generation { - let e_ptr: *mut DirEntry = std::ptr::from_mut::(*existing); - // SAFETY: BSSMap-owned `DirEntry` (boxed/leaked into `EntriesOption`); `entries_mutex` held. - let dir = unsafe { (*e_ptr).dir }; - // `open_dir_for_iteration`, NOT - // `RealFS.openDir`. On Windows the two diverge: `open_dir` passes - // `read_only: true` (no DELETE access on the handle), whereas - // `openDirForIteration` uses the default `WindowsOpenDirOptions` - // (`can_rename_or_delete: true`). On POSIX it's `O_DIRECTORY` only - // vs `O_RDONLY|O_DIRECTORY`. - let handle = match bun_sys::open_dir_for_iteration(Fd::cwd(), dir) { - Ok(h) => h, - Err(err) => { - // SAFETY: see above. - unsafe { (*e_ptr).data.clear() }; - return self.read_directory_error(dir, err.into()).ok(); - } - }; - let _close_guard = scopeguard::guard(handle, |h| { - let _ = bun_sys::close(h); - }); - // SAFETY: see above — exclusive `&mut` on the prev map for the duration of `readdir`. - let prev = Some(unsafe { &mut (*e_ptr).data }); - match self.readdir(false, prev, dir, generation, handle, ()) { + // Copy the stale slot's interned `dir` out and end the borrow; each step + // below re-indexes the slot instead of holding a `&mut` across a call that + // needs `&mut self`. `entries_mutex` held by caller. + let stale_dir: Option<&'static [u8]> = match self.entries.at_index(index)? { + EntriesOption::Entries(existing) if existing.generation < generation => { + Some(existing.dir) + } + _ => None, + }; + let Some(dir) = stale_dir else { + return self.entries.at_index(index); + }; + // `open_dir_for_iteration`, NOT + // `RealFS.openDir`. On Windows the two diverge: `open_dir` passes + // `read_only: true` (no DELETE access on the handle), whereas + // `openDirForIteration` uses the default `WindowsOpenDirOptions` + // (`can_rename_or_delete: true`). On POSIX it's `O_DIRECTORY` only + // vs `O_RDONLY|O_DIRECTORY`. + let handle = match bun_sys::open_dir_for_iteration(Fd::cwd(), dir) { + Ok(h) => h, + Err(err) => { + if let Some(EntriesOption::Entries(existing)) = self.entries.at_index(index) { + existing.data.clear(); + } + return self.read_directory_error(dir, err.into()).ok(); + } + }; + let _close_guard = scopeguard::guard(handle, |h| { + let _ = bun_sys::close(h); + }); + let readdir_err = match self.entries.at_index(index)? { + EntriesOption::Entries(existing) => { + let read = + Self::readdir(false, Some(&mut existing.data), dir, generation, handle, ()); + existing.data.clear(); + match read { Ok(new_entry) => { - // SAFETY: see above. - unsafe { (*e_ptr).data.clear() }; - // SAFETY: see above — slot is exclusively owned here. - unsafe { *e_ptr = new_entry }; - } - Err(err) => { - // SAFETY: see above. - unsafe { (*e_ptr).data.clear() }; - return self.read_directory_error(dir, err).ok(); + **existing = new_entry; + None } + Err(err) => Some(err), } } + EntriesOption::Err(_) => None, + }; + if let Some(err) = readdir_err { + return self.read_directory_error(dir, err).ok(); } - // SAFETY: BSSMap-owned slot; outlives caller (process-static singleton). - Some(unsafe { &mut *result_ptr }) + self.entries.at_index(index) } fn platform_temp_dir_compute() -> &'static [u8] { @@ -1949,8 +1948,8 @@ pub mod dir_entry_accessor { core::ptr::NonNull::new(*val).expect("EntryStore slot"), ); let fs: *mut Implementation = &raw mut FS::instance().fs; - // SAFETY: entries_mutex held; fs points at the process-global RealFS. - let kind = unsafe { entry.kind(fs, true) }; + // SAFETY: `fs` points at the process-global RealFS. + let kind = entry.kind(unsafe { bun_ptr::ParentRef::from_raw(fs) }, true); let fskind = match kind { EntryKind::File => bun_sys::FileKind::File, EntryKind::Dir => bun_sys::FileKind::Directory, @@ -2486,7 +2485,7 @@ pub mod cache { /// `Contents::Owned(Vec)` path. pub fn read_file_with_allocator( &mut self, - _fs: &mut fs_mod::FileSystem, + _fs: &fs_mod::FileSystem, path: &[u8], dirname_fd: Fd, use_shared_buffer: bool, diff --git a/src/resolver/package_json.rs b/src/resolver/package_json.rs index ebf773804acf..8040aafc35ea 100644 --- a/src/resolver/package_json.rs +++ b/src/resolver/package_json.rs @@ -421,14 +421,12 @@ impl PackageJSON { ) -> Option { let include_scripts = include_scripts_ == IncludeScripts::IncludeScripts; - // SAFETY: PORT (Stacked Borrows) — `r.fs()`/`r.log()` return RAW `*mut` - // (see `Resolver::fs()` note in lib.rs). `fs` and `log` are DISTINCT - // singletons so the two `&mut` projections below do not alias each other, - // and no other `&mut *r.fs` / `&mut *r.log` retag occurs while they are - // live in this function. Caller upholds the single-thread `Resolver` - // aliasing contract. - let r_fs: &mut fs::FileSystem = unsafe { &mut *r.fs() }; - // SAFETY: see above — `r.log()` points to a distinct singleton from `r.fs()`. + // BACKREF — the `FileSystem` singleton outlives the `Resolver`; every use + // below is a shared read, and the detached back-pointer leaves `r.caches` + // free to borrow mutably. + let r_fs: bun_ptr::BackRef = bun_ptr::BackRef::new(r.fs_ref()); + // SAFETY: BACKREF — `r.log()` is the owner-allocated `Log`, a distinct + // singleton from `r.fs()`; no other `&mut *r.log()` retag is live here. let r_log: &mut bun_ast::Log = unsafe { &mut *r.log() }; // TODO: remove this extra copy @@ -444,7 +442,7 @@ impl PackageJSON { // (allocator dropped — global mimalloc) let mut entry = match r.caches.fs.read_file_with_allocator( - r_fs, + &*r_fs, package_json_path, dirname_fd, false, @@ -659,7 +657,7 @@ impl PackageJSON { // import of "foo", but that's actually not a bug. Or arguably it's a // bug in Browserify but we have to replicate this bug because packages // do this in the wild. - let key: Box<[u8]> = FileSystemPackageJsonExt::normalize(r_fs, _key_str); + let key: Box<[u8]> = FileSystemPackageJsonExt::normalize(&*r_fs, _key_str); match &prop.value { js_ast::E::JsonValue::String(str) => { diff --git a/src/resolver/resolver.rs b/src/resolver/resolver.rs index 0c6c7618f556..afc3834a1284 100644 --- a/src/resolver/resolver.rs +++ b/src/resolver/resolver.rs @@ -1726,9 +1726,9 @@ impl<'a> Resolver<'a> { if let Some(entries) = dir.get_entries_ref(self.generation) { if let Some(query) = entries.get(name.filename) { - // SAFETY: entries_mutex held; rfs points at the process-global RealFS. - let symlink_path = - unsafe { query.entry().symlink(self.rfs_ptr(), self.store_fd) }; + // SAFETY: `rfs_ptr()` points at the process-global RealFS. + let fs = unsafe { bun_ptr::ParentRef::from_raw(self.rfs_ptr()) }; + let symlink_path = query.entry().symlink(fs, self.store_fd); if !symlink_path.is_empty() { path.set_realpath(symlink_path); if !result.file_fd.is_valid() { @@ -3578,16 +3578,18 @@ impl<'a> Resolver<'a> { unsafe { (*dir_entries_ptr).data.count() }, ); - dir_entries_option = rfs!() - .entries - .put( - &mut cached_dir_entry_result, - Fs::file_system::real_fs::EntriesOption::Entries( - // SAFETY: `dir_entries_ptr` is a live BSSMap slot (`in_place`) or a freshly boxed entry. - unsafe { &mut *dir_entries_ptr }, - ), - ) - .expect("unreachable"); + dir_entries_option = std::ptr::from_mut( + rfs!() + .entries + .put( + &mut cached_dir_entry_result, + Fs::file_system::real_fs::EntriesOption::Entries( + // SAFETY: `dir_entries_ptr` is a live BSSMap slot (`in_place`) or a freshly boxed entry. + unsafe { &mut *dir_entries_ptr }, + ), + ) + .expect("unreachable"), + ); } // We must initialize it as empty so that the result index is correct. @@ -3851,10 +3853,9 @@ impl<'a> Resolver<'a> { } }; - // SAFETY: entries_mutex held; rfs points at the process-global RealFS. - if unsafe { entry_query.entry().kind(self.rfs_ptr(), self.store_fd) } - == Fs::file_system::EntryKind::Dir - { + // SAFETY: `rfs_ptr()` points at the process-global RealFS. + let fs = unsafe { bun_ptr::ParentRef::from_raw(self.rfs_ptr()) }; + if entry_query.entry().kind(fs, self.store_fd) == Fs::file_system::EntryKind::Dir { let ends_with_star = esm_resolution.status == Status::ExactEndsWithStar; esm_resolution.status = Status::UnsupportedDirectoryImport; @@ -3872,8 +3873,10 @@ impl<'a> Resolver<'a> { file_name[index.len()..].copy_from_slice(ext); let index_query = dir_entries.get(&file_name[..]); if let Some(iq) = index_query { - // SAFETY: entries_mutex held; rfs points at the process-global RealFS. - if unsafe { iq.entry().kind(self.rfs_ptr(), self.store_fd) } + // SAFETY: `rfs_ptr()` points at the process-global RealFS. + let fs = + unsafe { bun_ptr::ParentRef::from_raw(self.rfs_ptr()) }; + if iq.entry().kind(fs, self.store_fd) == Fs::file_system::EntryKind::File { if let Some(debug) = self.debug_logs.as_mut() { @@ -3904,17 +3907,15 @@ impl<'a> Resolver<'a> { } let absolute_out_path: &[u8] = { - if entry_query.entry().abs_path.is_empty() { - // SAFETY: EntryStore-owned slot; resolver mutex held. RHS fully - // evaluated before LHS `&mut Entry` is materialized. - unsafe { &mut *entry_query.entry }.abs_path = Interned::from_static( + if entry_query.entry().abs_path.get().is_empty() { + entry_query.entry().abs_path.set(Interned::from_static( self.fs_ref() .dirname_store .append_slice(abs_esm_path) .expect("unreachable"), - ); + )); } - entry_query.entry().abs_path.as_bytes() + entry_query.entry().abs_path.get().as_bytes() }; let module_type = if let Some(pkg) = resolved_dir_info.package_json() { pkg.module_type @@ -4662,13 +4663,13 @@ impl<'a> Resolver<'a> { // freshly boxed entry (see block-wide note above). unsafe { (*dir_entries_ptr).data.count() }, ); - dir_entries_option = rfs!().entries.put( + dir_entries_option = std::ptr::from_mut(rfs!().entries.put( &mut cached_dir_entry_result, Fs::file_system::real_fs::EntriesOption::Entries( // SAFETY: `dir_entries_ptr` is a live BSSMap slot (`in_place`) or a freshly boxed entry. unsafe { &mut *dir_entries_ptr }, ), - )?; + )?); } // We must initialize it as empty so that the result index is correct. @@ -5283,24 +5284,26 @@ impl<'a> Resolver<'a> { if let Some(entries) = dir_info.get_entries_ref(self.generation) { if let Some(lookup) = entries.get(&base[..]) { - // SAFETY: entries_mutex held; rfs points at the process-global RealFS. - if unsafe { lookup.entry().kind(rfs, self.store_fd) } + // SAFETY: `rfs` points at the process-global RealFS. + if lookup + .entry() + // SAFETY: `rfs` is `addr_of_mut!((*self.fs).fs)` on the process-global + // FileSystem singleton — non-null and outliving this resolver. + .kind(unsafe { bun_ptr::ParentRef::from_raw(rfs) }, self.store_fd) == Fs::file_system::EntryKind::File { let out_buf: &[u8] = { - if lookup.entry().abs_path.is_empty() { + if lookup.entry().abs_path.get().is_empty() { let parts = [dir_info.abs_path, &base[..]]; let out_buf_ = self.fs_ref().abs_buf(&parts, bufs!(index)); - // SAFETY: EntryStore-owned slot; resolver mutex held. RHS fully - // evaluated before LHS `&mut Entry` is materialized. - unsafe { &mut *lookup.entry }.abs_path = Interned::from_static( + lookup.entry().abs_path.set(Interned::from_static( self.fs_ref() .dirname_store .append_slice(out_buf_) .expect("unreachable"), - ); + )); } - lookup.entry().abs_path.as_bytes() + lookup.entry().abs_path.get().as_bytes() }; if let Some(debug) = self.debug_logs.as_mut() { @@ -5779,27 +5782,30 @@ impl<'a> Resolver<'a> { } if let Some(query) = entries!().get(base) { - // SAFETY: entries_mutex held; rfs points at the process-global RealFS. - if unsafe { query.entry().kind(rfs, self.store_fd) } == Fs::file_system::EntryKind::File + // SAFETY: `rfs` points at the process-global RealFS. + if query + .entry() + // SAFETY: `rfs` is `addr_of_mut!((*self.fs).fs)` on the process-global + // FileSystem singleton — non-null and outliving this resolver. + .kind(unsafe { bun_ptr::ParentRef::from_raw(rfs) }, self.store_fd) + == Fs::file_system::EntryKind::File { if let Some(debug) = self.debug_logs.as_mut() { debug.add_note_fmt(format_args!("Found file \"{}\" ", bstr::BStr::new(base))); } let abs_path: &'static [u8] = { - if query.entry().abs_path.is_empty() { - let abs_path_parts = [query.entry().dir, query.entry().base()]; + if query.entry().abs_path.get().is_empty() { + let abs_path_parts = [query.entry().dir(), query.entry().base()]; let joined = self.fs_ref().abs_buf(&abs_path_parts, bufs!(load_as_file)); - // SAFETY: EntryStore-owned slot; resolver mutex held. RHS fully - // evaluated before LHS `&mut Entry` is materialized. - unsafe { &mut *query.entry }.abs_path = Interned::from_static( + query.entry().abs_path.set(Interned::from_static( self.fs_ref() .dirname_store .append_slice(joined) .expect("unreachable"), - ); + )); } - query.entry().abs_path.as_bytes() + query.entry().abs_path.get().as_bytes() }; dec_ret!(Some(LoadResult { @@ -5880,8 +5886,12 @@ impl<'a> Resolver<'a> { buffer[segment.len()..].copy_from_slice(ext_to_replace); if let Some(query) = entries!().get(&buffer[..]) { - // SAFETY: entries_mutex held; rfs points at the process-global RealFS. - if unsafe { query.entry().kind(rfs, self.store_fd) } + // SAFETY: `rfs` points at the process-global RealFS. + if query + .entry() + // SAFETY: `rfs` is `addr_of_mut!((*self.fs).fs)` on the process-global + // FileSystem singleton — non-null and outliving this resolver. + .kind(unsafe { bun_ptr::ParentRef::from_raw(rfs) }, self.store_fd) == Fs::file_system::EntryKind::File { if let Some(debug) = self.debug_logs.as_mut() { @@ -5893,11 +5903,8 @@ impl<'a> Resolver<'a> { dec_ret!(Some(LoadResult { path: { - if query.entry().abs_path.is_empty() { - // SAFETY: `dir` is `&'static [u8]` (DirnameStore-interned), - // copied out so no `&Entry` borrow survives into the - // `&mut Entry` write below. - let entry_dir = query.entry().dir; + if query.entry().abs_path.get().is_empty() { + let entry_dir = query.entry().dir(); let new_abs = if !entry_dir.is_empty() && entry_dir[entry_dir.len() - 1] == SEP { @@ -5919,11 +5926,9 @@ impl<'a> Resolver<'a> { .expect("unreachable"), ) }; - // SAFETY: EntryStore-owned slot; resolver mutex held. RHS - // fully evaluated above — sole `&mut Entry` for this write. - unsafe { &mut *query.entry }.abs_path = new_abs; + query.entry().abs_path.set(new_abs); } - query.entry().abs_path.as_bytes() + query.entry().abs_path.get().as_bytes() }, diff_case: query.diff_case, dirname_fd: entries!().fd, @@ -5987,8 +5992,13 @@ impl<'a> Resolver<'a> { } if let Some(query) = entries.get().get(file_name) { - // SAFETY: entries_mutex held; rfs points at the process-global RealFS. - if unsafe { query.entry().kind(rfs, self.store_fd) } == Fs::file_system::EntryKind::File + // SAFETY: `rfs` points at the process-global RealFS. + if query + .entry() + // SAFETY: `rfs` is `addr_of_mut!((*self.fs).fs)` on the process-global + // FileSystem singleton — non-null and outliving this resolver. + .kind(unsafe { bun_ptr::ParentRef::from_raw(rfs) }, self.store_fd) + == Fs::file_system::EntryKind::File { if let Some(debug) = self.debug_logs.as_mut() { debug.add_note_fmt(format_args!( @@ -6000,21 +6010,15 @@ impl<'a> Resolver<'a> { // now that we've found it, we allocate it. return Some(LoadResult { path: { - // SAFETY: EntryStore-owned slot; resolver mutex held. RHS is fully - // evaluated (shared reads) before the LHS `&mut Entry` is - // materialized for the write — no overlapping unique borrow. - unsafe { &mut *query.entry }.abs_path = if query.entry().abs_path.is_empty() - { - Interned::from_static( + if query.entry().abs_path.get().is_empty() { + query.entry().abs_path.set(Interned::from_static( self.fs_ref() .dirname_store .append_slice(&buffer[..]) .expect("unreachable"), - ) - } else { - query.entry().abs_path - }; - query.entry().abs_path.as_bytes() + )); + } + query.entry().abs_path.get().as_bytes() }, diff_case: query.diff_case, dirname_fd: entries.fd, @@ -6094,9 +6098,13 @@ impl<'a> Resolver<'a> { if let Some(entry) = entries!().get_comptime_query(b"node_modules") { info.flags.set_present( DirInfo::Flag::HasNodeModules, - // SAFETY: entries_mutex held; `rfs_ptr` points at the process-global RealFS. - unsafe { entry.entry().kind(rfs_ptr, self.store_fd) } - == Fs::file_system::EntryKind::Dir, + // SAFETY: `rfs_ptr` points at the process-global RealFS. + entry.entry().kind( + // SAFETY: `rfs` is `addr_of_mut!((*self.fs).fs)` on the process-global + // FileSystem singleton — non-null and outliving this resolver. + unsafe { bun_ptr::ParentRef::from_raw(rfs_ptr) }, + self.store_fd, + ) == Fs::file_system::EntryKind::Dir, ); } } @@ -6146,9 +6154,13 @@ impl<'a> Resolver<'a> { if info.is_node_modules() { if let Some(q) = entries!().get_comptime_query(b".bin") { - // SAFETY: entries_mutex held; `rfs_ptr` points at the process-global RealFS. - if unsafe { q.entry().kind(rfs_ptr, self.store_fd) } - == Fs::file_system::EntryKind::Dir + // SAFETY: `rfs_ptr` points at the process-global RealFS. + if q.entry().kind( + // SAFETY: `rfs` is `addr_of_mut!((*self.fs).fs)` on the process-global + // FileSystem singleton — non-null and outliving this resolver. + unsafe { bun_ptr::ParentRef::from_raw(rfs_ptr) }, + self.store_fd, + ) == Fs::file_system::EntryKind::Dir { // SAFETY: BIN_FOLDERS_LOADED is single-thread init-once; protected by RESOLVER_MUTEX held by callers. if !BIN_FOLDERS_LOADED.load(core::sync::atomic::Ordering::Acquire) { @@ -6240,7 +6252,10 @@ impl<'a> Resolver<'a> { // SAFETY: `rfs_ptr` points at the process-global RealFS; the lazy-stat // rewrite inside `symlink()` is serialized on `Entry.mutex`. - let mut symlink = unsafe { entry.symlink(rfs_ptr, self.store_fd) }; + let mut symlink = entry.symlink( + unsafe { bun_ptr::ParentRef::from_raw(rfs_ptr) }, + self.store_fd, + ); if !symlink.is_empty() { if let Some(logs) = self.debug_logs.as_mut() { let mut buf = Vec::new(); @@ -6304,8 +6319,13 @@ impl<'a> Resolver<'a> { // SAFETY: EntryStore-owned slot; `entries_mutex` held — read-only borrow, // dies (NLL) before any later `&mut` to this slot. let entry = lookup.entry(); - // SAFETY: entries_mutex held; `rfs_ptr` points at the process-global RealFS. - if unsafe { entry.kind(rfs_ptr, self.store_fd) } == Fs::file_system::EntryKind::File + // SAFETY: `rfs_ptr` points at the process-global RealFS. + if entry.kind( + // SAFETY: `rfs` is `addr_of_mut!((*self.fs).fs)` on the process-global + // FileSystem singleton — non-null and outliving this resolver. + unsafe { bun_ptr::ParentRef::from_raw(rfs_ptr) }, + self.store_fd, + ) == Fs::file_system::EntryKind::File { info.package_json = if self.use_package_manager() && !info.has_node_modules() @@ -6374,9 +6394,13 @@ impl<'a> Resolver<'a> { // SAFETY: EntryStore-owned slot; `entries_mutex` held — read-only borrow, // dies (NLL) before any later `&mut` to this slot. let entry = lookup.entry(); - // SAFETY: entries_mutex held; `rfs_ptr` points at the process-global RealFS. - if unsafe { entry.kind(rfs_ptr, self.store_fd) } - == Fs::file_system::EntryKind::File + // SAFETY: `rfs_ptr` points at the process-global RealFS. + if entry.kind( + // SAFETY: `rfs` is `addr_of_mut!((*self.fs).fs)` on the process-global + // FileSystem singleton — non-null and outliving this resolver. + unsafe { bun_ptr::ParentRef::from_raw(rfs_ptr) }, + self.store_fd, + ) == Fs::file_system::EntryKind::File { let parts = [path, b"tsconfig.json".as_slice()]; tsconfig_path = Some( @@ -6390,9 +6414,13 @@ impl<'a> Resolver<'a> { // SAFETY: EntryStore-owned slot; `entries_mutex` held — read-only borrow, // dies (NLL) before any later `&mut` to this slot. let entry = lookup.entry(); - // SAFETY: entries_mutex held; `rfs_ptr` points at the process-global RealFS. - if unsafe { entry.kind(rfs_ptr, self.store_fd) } - == Fs::file_system::EntryKind::File + // SAFETY: `rfs_ptr` points at the process-global RealFS. + if entry.kind( + // SAFETY: `rfs` is `addr_of_mut!((*self.fs).fs)` on the process-global + // FileSystem singleton — non-null and outliving this resolver. + unsafe { bun_ptr::ParentRef::from_raw(rfs_ptr) }, + self.store_fd, + ) == Fs::file_system::EntryKind::File { let parts = [path, b"jsconfig.json".as_slice()]; tsconfig_path = Some( diff --git a/src/router/lib.rs b/src/router/lib.rs index 13873969f773..3516c5298a8c 100644 --- a/src/router/lib.rs +++ b/src/router/lib.rs @@ -44,7 +44,7 @@ mod api { type CoreError = bun_core::Error; use bun_core::HashedString; -use bun_ptr::Interned; +use bun_ptr::{Interned, ParentRef}; // ────────────────────────────────────────────────────────────────────────── // cross-tier decoupling @@ -1503,7 +1503,7 @@ pub trait ResolverLike { fn fs(&self) -> &'static FileSystem; /// The resolver's `Implementation` field, passed to /// `Entry.kind` for lazy stat. - fn fs_impl(&self) -> *mut Fs::Implementation; + fn fs_impl(&self) -> ParentRef; /// Returns an arena handle (not a borrow) so the resolver's `&mut self` /// borrow ends before the recursive `load()` re-borrows it. fn read_dir_info_ignore_error(&mut self, path: &[u8]) -> Option; @@ -2078,9 +2078,8 @@ mod tests { // SAFETY: process-static singleton (see `FileSystem::instance`). unsafe { &*self.0.fs() } } - fn fs_impl(&self) -> *mut Fs::Implementation { - // SAFETY: `&fs.fs` — the `Implementation` field of the singleton. - unsafe { core::ptr::from_mut(&mut (*self.0.fs()).fs) } + fn fs_impl(&self) -> ParentRef { + ParentRef::new(&self.0.fs_ref().fs) } fn read_dir_info_ignore_error(&mut self, path: &[u8]) -> Option { self.0.read_dir_info_ignore_error(path) @@ -2155,10 +2154,9 @@ mod tests { .ok_or_else(|| bun_core::err!("FileNotFound"))?; // return RouteLoader.loadAll(..., opts.routes, &logger, Resolver, &resolver, root_dir); - // SAFETY: `_err_dump` only re-derives `&*log` on drop (after this borrow ends). let routes = RouteLoader::load_all( router.config.clone(), - unsafe { &mut *core::ptr::from_mut(&mut log) }, + &mut log, &mut resolver, &root_dir, top_level_dir, @@ -2220,13 +2218,7 @@ mod tests { .ok_or_else(|| bun_core::err!("FileNotFound"))?; // try router.loadRoutes(&logger, root_dir, Resolver, &resolver, top_level_dir); - // SAFETY: `_err_dump` only re-derives `&*log` on drop (after this borrow ends). - router.load_routes( - unsafe { &mut *core::ptr::from_mut(&mut log) }, - &root_dir, - &mut resolver, - top_level_dir, - )?; + router.load_routes(&mut log, &root_dir, &mut resolver, top_level_dir)?; let entry_points = router.get_entry_points(); assert_eq!(data.len(), entry_points.len()); diff --git a/src/runtime/allocators/LinuxMemFdAllocator.rs b/src/runtime/allocators/LinuxMemFdAllocator.rs index e48ff34afa5b..33eafa0c5ca5 100644 --- a/src/runtime/allocators/LinuxMemFdAllocator.rs +++ b/src/runtime/allocators/LinuxMemFdAllocator.rs @@ -35,23 +35,15 @@ use crate::webcore::blob::store::Bytes as BlobStoreBytes; // through `StdAllocator.ptr`) cross threads, so the single-threaded `RefCount` // flavor would data-race on ref/deref. #[derive(bun_ptr::ThreadSafeRefCounted)] -#[ref_count(destroy = Self::deinit)] pub struct LinuxMemFdAllocator { ref_count: bun_ptr::ThreadSafeRefCount, pub fd: Fd, pub size: usize, } -impl LinuxMemFdAllocator { - /// Close the fd, then free the allocation. - /// - /// # Safety - /// Refcount hit 0; `this` came from `heap::alloc` in `IntrusiveArc::new`. - unsafe fn deinit(this: *mut Self) { - // SAFETY: sole owner — close fd before reclaiming the Box. - unsafe { (*this).fd.close() }; - // SAFETY: sole owner; reconstruct the Box so the allocation is freed. - drop(unsafe { bun_core::heap::take(this) }); +impl Drop for LinuxMemFdAllocator { + fn drop(&mut self) { + self.fd.close(); } } diff --git a/src/runtime/api/Archive.rs b/src/runtime/api/Archive.rs index 4030547c0498..3058bae6ddd2 100644 --- a/src/runtime/api/Archive.rs +++ b/src/runtime/api/Archive.rs @@ -755,19 +755,14 @@ impl AsyncTask { } } - /// # Safety - /// `this` must be the live `heap::into_raw` allocation produced by - /// [`create`](Self::create), called exactly once on the JS thread after - /// `run_callback` enqueues it. Takes ownership of the allocation. - // Forwards `this` to `bun_core::heap::take` without dereferencing it here; - // not_unsafe_ptr_arg_deref is a false positive on opaque-token forwarding. - #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub fn run_from_js(this: *mut Self) -> Result<(), bun_jsc::JsTerminated> { - // SAFETY: see fn-level safety contract. - let mut owned = unsafe { bun_core::heap::take(this) }; - owned.keep_alive.unref(bun_io::js_vm_ctx()); - - // `defer { ctx.deinit; destroy(this) }` — handled by `owned: Box` dropping at scope + /// Consumes the allocation produced by [`create`](Self::create); called + /// exactly once on the JS thread after `run_callback` enqueues it. + // `boxed_local`: the `Box` is the ownership unit being reclaimed here. + #[allow(clippy::boxed_local)] + pub fn run_from_js(mut self: Box) -> Result<(), bun_jsc::JsTerminated> { + self.keep_alive.unref(bun_io::js_vm_ctx()); + + // `defer { ctx.deinit; destroy(this) }` — handled by `self: Box` dropping at scope // exit (ctx implements Drop). let vm = VirtualMachine::get(); @@ -776,8 +771,8 @@ impl AsyncTask { } let global = vm.global(); - let promise = owned.promise.swap(); - let result = match owned.ctx.run_from_js(global) { + let promise = self.promise.swap(); + let result = match self.ctx.run_from_js(global) { Ok(r) => r, Err(e) => { // JSError means exception is already pending @@ -1254,7 +1249,7 @@ impl TaskContext for FilesContext { match &mut self.result { FilesResult::Success(entries) => { let map = JSMap::create(global); - let Some(mut map_ptr) = JSMap::from_js(map) else { + let Some(map_ptr) = JSMap::from_js(map) else { return Ok(PromiseResult::Reject( global.create_error_instance(format_args!("Failed to create Map")), )); @@ -1265,15 +1260,14 @@ impl TaskContext for FilesContext { let blob_ptr = Blob::new(Blob::create_with_bytes_and_allocator(data, global, false)); // SAFETY: blob_ptr is the heap allocation just produced by Blob::new. - let blob = unsafe { &mut *blob_ptr }; + let blob = unsafe { &*blob_ptr }; blob.is_jsdom_file.set(true); blob.name.set(bun_core::String::clone_utf8(&entry.path)); blob.last_modified.set((entry.mtime * 1000) as f64); let name_js = blob.name.get().to_js(global)?; let blob_js = blob.to_js(global); - // SAFETY: map_ptr came from JSMap::from_js on a live value. - unsafe { map_ptr.as_mut() }.set(global, name_js, blob_js)?; + JSMap::opaque_ref(map_ptr.as_ptr()).set(global, name_js, blob_js)?; } Ok(PromiseResult::Resolve(map)) diff --git a/src/runtime/api/BunObject.rs b/src/runtime/api/BunObject.rs index 664c07c2046b..d1adbce1748e 100644 --- a/src/runtime/api/BunObject.rs +++ b/src/runtime/api/BunObject.rs @@ -1607,10 +1607,12 @@ pub(crate) fn serve(global_object: &JSGlobalObject, callframe: &CallFrame) -> Js if let Some(entry) = hot.get_entry(&config.id) { macro_rules! reload { ($T:ty) => {{ + let ptr = entry.ptr.cast::<$T>(); // SAFETY: tag was matched; ptr was inserted as `*mut $T` below. - let server: &mut $T = unsafe { &mut *entry.ptr.cast::<$T>() }; - server.on_reload_from_zig(&mut config, global_object); - return Ok(server.js_value.try_get().unwrap_or(JSValue::UNDEFINED)); + unsafe { &mut *ptr }.on_reload_from_zig(&mut config, global_object); + // SAFETY: fresh shared borrow, after the reload's JS re-entry. + let js_value = unsafe { &*ptr }.js_value.get().try_get(); + return Ok(js_value.unwrap_or(JSValue::UNDEFINED)); }}; } match entry.tag { @@ -1634,8 +1636,6 @@ pub(crate) fn serve(global_object: &JSGlobalObject, callframe: &CallFrame) -> Js if global_object.has_exception() { return Ok(JSValue::ZERO); } - // SAFETY: `init` returned a live heap-allocated server pointer. - let server_ref: &mut $ServerType = unsafe { &mut *server }; // SAFETY: `server` is the live heap-allocated server returned by `init`. let route_list_object = <$ServerType>::listen(server); if global_object.has_exception() { @@ -1648,7 +1648,12 @@ pub(crate) fn serve(global_object: &JSGlobalObject, callframe: &CallFrame) -> Js // `server_body` until per-type codegen externs land. <$ServerType>::js_gc_route_list_set(obj, global_object, route_list_object); } - server_ref.js_value.set_strong(obj, global_object); + // SAFETY: `init` returned a live heap-allocated server pointer; this + // shared borrow starts after every JS-reaching call above. + let server_ref: &$ServerType = unsafe { &*server }; + server_ref + .js_value + .with_mut(|r| r.set_strong(obj, global_object)); if global_object.bun_vm().test_isolation_enabled { if let Some(handles) = crate::jsc_hooks::isolation_handles() { @@ -2966,8 +2971,8 @@ pub mod JSZstd { }, ) .expect("ZstdCtx::init is infallible"); - // SAFETY: `job` is a freshly-created live pointer. - unsafe { jsc::AnyTaskJob::schedule(job) }; + // SAFETY: `job` is a freshly-created, unscheduled, owned allocation. + jsc::AnyTaskJob::schedule(unsafe { bun_core::heap::take(job) }); promise_value } @@ -3030,7 +3035,7 @@ mod stdio_stores { mode, ..Default::default() }), - mime_type: bun_http_types::MimeType::NONE, + mime_type: bun_jsc::JsCell::new(bun_http_types::MimeType::NONE), ref_count: bun_ptr::ThreadSafeRefCount::init(), is_all_ascii: None, }); diff --git a/src/runtime/api/JSBundler.rs b/src/runtime/api/JSBundler.rs index d44b40d003bb..99c81830907a 100644 --- a/src/runtime/api/JSBundler.rs +++ b/src/runtime/api/JSBundler.rs @@ -1405,10 +1405,12 @@ pub mod js_bundler { /// disjoint from `Resolve`/`Load`, so the returned `&mut` does not alias /// the caller's `&mut Resolve`/`&mut Load`. #[inline] - fn bv2_mut<'a>(bv2: *mut BundleV2<'static>) -> &'a mut BundleV2<'static> { + fn bv2_mut<'a>( + bv2: Option>>, + ) -> &'a mut BundleV2<'static> { // SAFETY: see fn doc — live backref (owner-creates-child), single // JS-thread, disjoint heap from the `Resolve`/`Load` callers borrow. - unsafe { &mut *bv2 } + unsafe { bv2.expect("bv2").assume_mut() } } /// `&mut Plugin` for the live `BundleV2` backref stored on `Resolve`/`Load`. @@ -1421,9 +1423,11 @@ pub mod js_bundler { /// disjoint from `Resolve`/`Load`, so the returned `&mut` does not alias /// the caller's `&mut Resolve`/`&mut Load`. #[inline] - fn bv2_plugin<'a>(bv2: *mut BundleV2<'static>) -> &'a mut Plugin { - // SAFETY: see fn doc — `plugins.is_some()`, disjoint heap. - unsafe { &mut *bv2_mut(bv2).plugins.unwrap().as_ptr() } + fn bv2_plugin<'a>(bv2: Option>>) -> &'a mut Plugin { + let bv2 = bv2.expect("bv2"); + // SAFETY: `plugins.is_some()`; the opaque C++ plugin is a distinct + // allocation from the bundle, so no `&mut BundleV2` is taken here. + unsafe { &mut *bv2.get().plugins.unwrap().as_ptr() } } /// # Safety @@ -1532,7 +1536,7 @@ pub mod js_bundler { } fn on_notify_defer_raw(ctx: *mut BundleV2<'static>) -> bun_event_loop::JsResult<()> { - bv2_mut(ctx).on_notify_defer(); + bv2_mut(core::ptr::NonNull::new(ctx).map(bun_ptr::ParentRef::from)).on_notify_defer(); Ok(()) } @@ -1571,9 +1575,10 @@ pub mod js_bundler { if this.was_file { // Faster path: skip the extra threadpool dispatch + let bv2 = this.bv2.expect("bv2").as_mut_ptr(); // SAFETY: bv2 backref is valid; pool/worker_pool are live for bundle. unsafe { - (*(*(*this.bv2).graph.pool.as_ptr()).worker_pool).schedule( + (*(*(*bv2).graph.pool.as_ptr()).worker_pool).schedule( bun_threading::thread_pool::Batch::from(core::ptr::addr_of_mut!( (*this.parse_task.as_ptr()).task )), diff --git a/src/runtime/api/JSTranspiler.rs b/src/runtime/api/JSTranspiler.rs index 0f9cd0ff6a27..9df7e326191d 100644 --- a/src/runtime/api/JSTranspiler.rs +++ b/src/runtime/api/JSTranspiler.rs @@ -1134,7 +1134,7 @@ impl Drop for JSTranspiler { /// and early `return Err`) those must be restored before the locals drop, or /// the next method call dereferences a dangling allocator/log. struct TranspilerStateGuard { - transpiler: *mut Transpiler::Transpiler<'static>, + transpiler: bun_ptr::ParentRef>, prev_arena: &'static Arena, restore_log: *mut bun_ast::Log, /// `Some(prev)` ⇒ also restore `macro_context` to `prev` (transformSync's @@ -1145,20 +1145,12 @@ struct TranspilerStateGuard { impl TranspilerStateGuard { /// Mutable access to the guarded `Transpiler`. - /// - /// SAFETY: `self.transpiler` is always non-null — every construction site - /// initializes it from `js_transpiler.transpiler.as_ptr()` (the - /// `JsCell` in the heap-stable `Box`), which - /// outlives this stack-local guard. The guard is held as - /// `let _restore = ...;` and never touched between construction and `Drop`, - /// so no other `&mut Transpiler` projection from that `JsCell` is live when - /// this runs. #[inline] fn transpiler_mut(&mut self) -> &mut Transpiler::Transpiler<'static> { - // SAFETY: `self.transpiler` is non-null (set from `JsCell::as_ptr()` on the - // heap-stable `Box`); the guard holds the only live `&mut` - // projection of that `JsCell` between construction and `Drop`. - unsafe { &mut *self.transpiler } + // SAFETY: built by `from_raw_mut` from `JsCell::as_ptr()` on the heap-stable + // `Box`, which outlives this guard; the guard holds the only + // live `&mut` projection of that `JsCell` between construction and `Drop`. + unsafe { self.transpiler.assume_mut() } } /// Raw `*mut Log` to restore on drop. Returned as a pointer (not `&mut`) @@ -1357,7 +1349,9 @@ impl JSTranspiler { prev }); let _restore = TranspilerStateGuard { - transpiler: self.transpiler.as_ptr(), + // SAFETY: write provenance from `JsCell::as_ptr()`; the cell lives in the + // heap-stable `Box` and outlives this stack-local guard. + transpiler: unsafe { bun_ptr::ParentRef::from_raw_mut(self.transpiler.as_ptr()) }, prev_arena, restore_log: self.config_log_ptr(), prev_macro_context: None, @@ -1555,7 +1549,9 @@ impl JSTranspiler { (prev_arena, prev_mc) }); let _restore = TranspilerStateGuard { - transpiler: self.transpiler.as_ptr(), + // SAFETY: write provenance from `JsCell::as_ptr()`; the cell lives in the + // heap-stable `Box` and outlives this stack-local guard. + transpiler: unsafe { bun_ptr::ParentRef::from_raw_mut(self.transpiler.as_ptr()) }, prev_arena, restore_log: self.config_log_ptr(), prev_macro_context: Some(prev_macro_context), @@ -1741,7 +1737,9 @@ impl JSTranspiler { prev }); let _restore = TranspilerStateGuard { - transpiler: self.transpiler.as_ptr(), + // SAFETY: write provenance from `JsCell::as_ptr()`; the cell lives in the + // heap-stable `Box` and outlives this stack-local guard. + transpiler: unsafe { bun_ptr::ParentRef::from_raw_mut(self.transpiler.as_ptr()) }, prev_arena, restore_log: self.config_log_ptr(), prev_macro_context: None, diff --git a/src/runtime/api/YAMLObject.rs b/src/runtime/api/YAMLObject.rs index 5a72dcfbf3c1..b3b9acee4871 100644 --- a/src/runtime/api/YAMLObject.rs +++ b/src/runtime/api/YAMLObject.rs @@ -1123,9 +1123,7 @@ impl From for ToJsError { impl<'a> ParserCtx<'a> { // deinit: seen_objects has Drop; no explicit impl needed. - extern "C" fn run(ctx: *mut ParserCtx<'a>, args: *mut MarkedArgumentBuffer) { - // SAFETY: MarkedArgumentBuffer::run passes valid non-null pointers for the duration of the call - let (ctx, args) = unsafe { (&mut *ctx, &mut *args) }; + extern "C" fn run(ctx: &mut ParserCtx<'a>, args: &mut MarkedArgumentBuffer) { let root = ctx.root; ctx.result = match ctx.to_js(args, root) { Ok(v) => v, diff --git a/src/runtime/api/bun/SSLContextCache.rs b/src/runtime/api/bun/SSLContextCache.rs index 92a9401d0717..a85b8eb4a34a 100644 --- a/src/runtime/api/bun/SSLContextCache.rs +++ b/src/runtime/api/bun/SSLContextCache.rs @@ -19,6 +19,7 @@ //! `tls.ts` SHA-256/WeakRef memo: every path that turns an `SSLConfig` into an //! `SSL_CTX*` goes through here, so one config = one CTX per process. +use core::cell::Cell; use core::ffi::{c_int, c_long, c_void}; use core::ptr; @@ -62,7 +63,7 @@ pub struct Entry { /// Nulled by `bun_ssl_ctx_cache_on_free` when BoringSSL drops the last /// ref. Tombstoned entries are reclaimed on the next `get_or_create` for /// the same digest, or by the periodic compact. - pub ctx: *mut boringssl::SSL_CTX, + pub ctx: Cell<*mut boringssl::SSL_CTX>, /// BACKREF: the cache outlives every `Entry` it allocates (Drop clears /// ex_data first so the `CRYPTO_EX_free` callback never sees a dangling /// owner). @@ -105,8 +106,8 @@ impl SSLContextCache { // SAFETY: map values are live heap Entries (heap::alloc below); freed only // via compact_locked / Drop, both of which hold this mutex. let entry = unsafe { &**entry }; - if !entry.ctx.is_null() { - let ctx = entry.ctx; + let ctx = entry.ctx.get(); + if !ctx.is_null() { // SAFETY: ctx non-null and tombstone write is serialized by this mutex. unsafe { boringssl::SSL_CTX_up_ref(ctx) }; return Some(ctx); @@ -131,10 +132,13 @@ impl SSLContextCache { // Prefer the already-cached one and drop ours so callers converge. let gop = bun_core::handle_oom(self.map.get_or_put(d)); if gop.found_existing { + let entry_ptr: *mut Entry = *gop.value_ptr; // SAFETY: existing map value is a live heap Entry (see above). - let entry = unsafe { &mut **gop.value_ptr }; - if !entry.ctx.is_null() { - let existing = entry.ctx; + let entry = unsafe { &*entry_ptr }; + // Read the slot out before SSL_CTX_free below, which can re-enter + // `bun_ssl_ctx_cache_on_free` and write this same `ctx` cell. + let existing = entry.ctx.get(); + if !existing.is_null() { // SAFETY: existing non-null; ctx is the fresh CTX we just built and own. unsafe { boringssl::SSL_CTX_up_ref(existing); @@ -146,23 +150,23 @@ impl SSLContextCache { // SSL_CTX_set_ex_data only fails on OOM (Bun crashes anyway), but if // it did, the entry would never tombstone and `entry.ctx` would dangle // after the CTX is freed. Don't cache it; caller still owns the ref. - // SAFETY: ctx is a valid SSL_CTX*; entry is a valid heap pointer. + // SAFETY: ctx is a valid SSL_CTX*; entry_ptr is a valid heap pointer. if unsafe { boringssl::SSL_CTX_set_ex_data( ctx, c::us_ssl_ctx_cache_ex_idx(), - std::ptr::from_mut::(entry).cast::(), + entry_ptr.cast::(), ) } != 1 { return Some(ctx); } - entry.ctx = ctx; + entry.ctx.set(ctx); return Some(ctx); } let entry = bun_core::heap::into_raw(Box::new(Entry { - ctx, + ctx: Cell::new(ctx), owner: owner_ptr, })); *gop.value_ptr = entry; @@ -195,7 +199,7 @@ impl SSLContextCache { while i < self.map.count() { let entry = self.map.values()[i]; // SAFETY: map values are live heap Entries; we hold the mutex. - if unsafe { (*entry).ctx.is_null() } { + if unsafe { (*entry).ctx.get().is_null() } { // SAFETY: entry was heap-allocated in get_or_create_digest; ex_data // back-pointer is already moot (ctx == null means CRYPTO_EX_free ran). drop(unsafe { bun_core::heap::take(entry) }); @@ -236,9 +240,9 @@ pub extern "C" fn bun_ssl_ctx_cache_on_free( } // SAFETY: non-null ptr is the *Entry we stored via SSL_CTX_set_ex_data; the // owning cache outlives every SSL_CTX it hands out (Drop clears ex_data first). - let entry: &mut Entry = unsafe { bun_ptr::callback_ctx::(ptr) }; + let entry: &Entry = unsafe { &*ptr.cast::() }; let _guard = entry.owner.mutex.lock_guard(); - entry.ctx = ptr::null_mut(); + entry.ctx.set(ptr::null_mut()); } impl Drop for SSLContextCache { @@ -251,11 +255,12 @@ impl Drop for SSLContextCache { for &entry in self.map.values() { // SAFETY: map values are live heap Entries; we hold the mutex. let e = unsafe { &*entry }; - if !e.ctx.is_null() { + let ctx = e.ctx.get(); + if !ctx.is_null() { // SAFETY: ctx non-null; clearing the ex_data slot we set. unsafe { boringssl::SSL_CTX_set_ex_data( - e.ctx, + ctx, c::us_ssl_ctx_cache_ex_idx(), ptr::null_mut(), ); diff --git a/src/runtime/api/bun/SecureContext.rs b/src/runtime/api/bun/SecureContext.rs index ab44fd62145d..d09aadc1279b 100644 --- a/src/runtime/api/bun/SecureContext.rs +++ b/src/runtime/api/bun/SecureContext.rs @@ -297,8 +297,8 @@ impl SecureContext { // SAFETY: `state` is the boxed per-thread `RuntimeState` installed by // `init_runtime_state`; the embedded `ssl_ctx_cache` has a stable // address for the VM's lifetime and is only touched from the JS thread. - let cache = unsafe { &mut (*state).ssl_ctx_cache }; - let Some(ctx) = cache.get_or_create_digest(ctx_opts, d, &mut err) else { + let cache = unsafe { &(*state).ssl_ctx_cache }; + let Some(ctx) = cache.with_mut(|c| c.get_or_create_digest(ctx_opts, d, &mut err)) else { // `err` is only set for the input-validation paths (bad PEM, missing // file, …). When BoringSSL itself fails (e.g. unsupported curve) the // enum is still `.none`; surface the library error stack instead of diff --git a/src/runtime/api/bun/h2_frame_parser.rs b/src/runtime/api/bun/h2_frame_parser.rs index 07dbf0510c53..4188db5675fa 100644 --- a/src/runtime/api/bun/h2_frame_parser.rs +++ b/src/runtime/api/bun/h2_frame_parser.rs @@ -1249,11 +1249,11 @@ impl Drop for DispatchGuard<'_> { } } -/// A `&mut Stream` that only exists inside an armed dispatch scope (`enter_stream_dispatch`): +/// A `&Stream` that only exists inside an armed dispatch scope (`enter_stream_dispatch`): /// while it is live, rewrite_read defers stream frees, so user JS that re-enters `read()` /// (option getters, header-value `toString`) cannot free the stream out from under the borrow. struct GuardedStream<'a> { - stream: &'a mut Stream, + stream: &'a Stream, _dispatch: DispatchGuard<'a>, } @@ -1264,12 +1264,6 @@ impl core::ops::Deref for GuardedStream<'_> { } } -impl core::ops::DerefMut for GuardedStream<'_> { - fn deref_mut(&mut self) -> &mut Stream { - self.stream - } -} - // R-2 (host-fn re-entrancy): every JS-exposed method takes `&self`; per-field // interior mutability via `Cell` (Copy) / `JsCell` (non-Copy). The codegen // shim still emits `this: &mut H2FrameParser` until Phase 1 lands — @@ -1489,39 +1483,41 @@ enum StreamState { CLOSED = 7, } +// Every field is interior-mutable so a `*mut Stream` from `streams` only ever needs to be +// reborrowed as `&Stream`: no `&mut` tag can be invalidated by JS re-entering a host fn. pub struct Stream { id: u32, - state: StreamState, - js_context: StrongOptional, // jsc.Strong.Optional - wait_for_trailers: bool, - end_after_headers: bool, - is_waiting_more_headers: bool, - header_block_size: usize, - header_block_count: usize, + state: Cell, + js_context: JsCell, // jsc.Strong.Optional + wait_for_trailers: Cell, + end_after_headers: Cell, + is_waiting_more_headers: Cell, + header_block_size: Cell, + header_block_count: Cell, // Header block fragments buffered across HEADERS + CONTINUATION until // END_HEADERS arrives (RFC 9113 §4.3); capped at `max_header_list_size`. - pending_header_block: Vec, + pending_header_block: JsCell>, // Flags from the HEADERS frame that started `pending_header_block`; // CONTINUATION frames only carry END_HEADERS. - pending_header_flags: u8, - padding: Option, - padding_strategy: PaddingStrategy, - rst_code: u32, - stream_dependency: u32, - exclusive: bool, - weight: u16, + pending_header_flags: Cell, + padding: Cell>, + padding_strategy: Cell, + rst_code: Cell, + stream_dependency: Cell, + exclusive: Cell, + weight: Cell, // current window size for the stream - window_size: u64, + window_size: Cell, // used window size for the stream - used_window_size: u64, + used_window_size: Cell, // remote window size for the stream - remote_window_size: u64, + remote_window_size: Cell, // remote used window size for the stream - remote_used_window_size: u64, - signal: Option>, + remote_used_window_size: Cell, + signal: JsCell>>, // when we have backpressure we queue the data e round robin the Streams - data_frame_queue: PendingQueue, + data_frame_queue: JsCell, } pub(crate) struct SignalRef { @@ -1560,8 +1556,8 @@ impl SignalRef { return; }; // SAFETY: stream is a *mut Stream from self.streams (heap::alloc); valid while the map entry exists - let stream = unsafe { &mut *stream }; - if stream.state != StreamState::CLOSED { + let stream = unsafe { &*stream }; + if stream.state.get() != StreamState::CLOSED { let wrapped = Bun__wrapAbortError(&parser.global_this, reason); parser.abort_stream(stream, wrapped); } @@ -1655,7 +1651,7 @@ impl PendingFrame { impl Stream { pub fn get_padding(&self, frame_len: usize, max_len: usize) -> u8 { - match self.padding_strategy { + match self.padding_strategy.get() { PaddingStrategy::None => 0, PaddingStrategy::Aligned => { let diff = (frame_len + 9) % 8; @@ -1672,17 +1668,18 @@ impl Stream { } } - pub fn flush_queue(&mut self, client: &H2FrameParser, written: &mut usize) -> FlushState { + pub fn flush_queue(&self, client: &H2FrameParser, written: &mut usize) -> FlushState { if !self.can_send_data() { // empty or cannot send data return FlushState::NoAction; } // try to flush one frame - let Some(front) = self.data_frame_queue.peek_front() else { + let Some((frame_len, frame_remaining)) = self + .data_frame_queue + .with_mut(|q| q.peek_front().map(|f| (f.len, f.slice().len()))) + else { return FlushState::NoAction; }; - let frame_len = front.len; - let frame_remaining = front.slice().len(); let mut owned_frame: Option = None; let no_backpressure: bool = 'brk: { @@ -1690,12 +1687,12 @@ impl Stream { if frame_len == 0 { // flush a zero payload frame - let Some(frame) = self.data_frame_queue.dequeue() else { + let Some(frame) = self.data_frame_queue.with_mut(|q| q.dequeue()) else { return FlushState::NoAction; }; let data_header = FrameHeader { type_: FrameType::HTTP_FRAME_DATA as u8, - flags: if frame.end_stream && !self.wait_for_trailers { + flags: if frame.end_stream && !self.wait_for_trailers.get() { DataFrameFlags::END_STREAM as u8 } else { 0 @@ -1710,7 +1707,8 @@ impl Stream { .min( (self .remote_window_size - .saturating_sub(self.remote_used_window_size)) + .get() + .saturating_sub(self.remote_used_window_size.get())) as usize, ) .min( @@ -1726,8 +1724,8 @@ impl Stream { H2FrameParser, "dataFrame flow control limited {} {} {} {} {} {}", frame_remaining, - self.remote_window_size, - self.remote_used_window_size, + self.remote_window_size.get(), + self.remote_used_window_size.get(), client.remote_window_size.get(), client.remote_used_window_size.get(), max_size @@ -1743,11 +1741,14 @@ impl Stream { } if max_size < frame_remaining { // we need to break the frame into smaller chunks - let Some(frame) = self.data_frame_queue.peek_front() else { + let Some(able_to_send) = self.data_frame_queue.with_mut(|q| { + let frame = q.peek_front()?; + let able_to_send = frame.slice()[0..max_size].to_vec(); + frame.offset += u32::try_from(max_size).expect("int cast"); + Some(able_to_send) + }) else { return FlushState::NoAction; }; - let able_to_send = frame.slice()[0..max_size].to_vec(); - frame.offset += u32::try_from(max_size).expect("int cast"); client .queued_data_size .set(client.queued_data_size.get() - able_to_send.len() as u64); @@ -1768,7 +1769,8 @@ impl Stream { max_size, payload_size ); - self.remote_used_window_size += payload_size as u64; + self.remote_used_window_size + .set(self.remote_used_window_size.get() + payload_size as u64); client .remote_used_window_size .set(client.remote_used_window_size.get() + payload_size as u64); @@ -1804,7 +1806,7 @@ impl Stream { } } else { // flush with some payload - owned_frame = self.data_frame_queue.dequeue(); + owned_frame = self.data_frame_queue.with_mut(|q| q.dequeue()); let Some(frame) = owned_frame.as_ref() else { return FlushState::NoAction; }; @@ -1829,12 +1831,13 @@ impl Stream { max_size, payload_size ); - self.remote_used_window_size += payload_size as u64; + self.remote_used_window_size + .set(self.remote_used_window_size.get() + payload_size as u64); client .remote_used_window_size .set(client.remote_used_window_size.get() + payload_size as u64); client.note_engine_send_consumed(self.id, payload_size as u64); - let mut flags: u8 = if frame.end_stream && !self.wait_for_trailers { + let mut flags: u8 = if frame.end_stream && !self.wait_for_trailers.get() { DataFrameFlags::END_STREAM as u8 } else { 0 @@ -1880,22 +1883,22 @@ impl Stream { if let Some(callback_value) = _frame.callback.get() { client.dispatch_write_callback(callback_value); } - if self.data_frame_queue.is_empty() { + if self.data_frame_queue.get().is_empty() { if _frame.end_stream { - if self.wait_for_trailers { + if self.wait_for_trailers.get() { client.dispatch(JSH2FrameParser::Gc::onWantTrailers, self.get_identifier()); } else { let identifier = self.get_identifier(); identifier.ensure_still_alive(); - if self.state == StreamState::HALF_CLOSED_REMOTE { - self.state = StreamState::CLOSED; + if self.state.get() == StreamState::HALF_CLOSED_REMOTE { + self.state.set(StreamState::CLOSED); } else { - self.state = StreamState::HALF_CLOSED_LOCAL; + self.state.set(StreamState::HALF_CLOSED_LOCAL); } client.dispatch_with_extra( JSH2FrameParser::Gc::onStreamEnd, identifier, - JSValue::js_number(self.state as u8 as f64), + JSValue::js_number(self.state.get() as u8 as f64), ); } } @@ -1911,7 +1914,7 @@ impl Stream { } pub fn queue_frame( - &mut self, + &self, client: &H2FrameParser, bytes: &[u8], callback: JSValue, @@ -1919,95 +1922,72 @@ impl Stream { ) { let global_this = client.global_this; - // Note: `dispatch_write_callback()` below re-enters JS, which can - // call back into `H2FrameParser` host-fns (e.g. `writeStream`) that - // look this `Stream` up by id from `client.streams` and reach - // `queue_frame()` again with a fresh `&mut Stream` aliasing this one. - // R-2: `client` is now `&H2FrameParser` (UnsafeCell-backed fields), so - // the parser-side noalias miscompile is structurally impossible. The - // `Stream`-side `&mut self` alias across re-entry remains; keep the - // `black_box` launder on `self`/`last_frame` as defense-in-depth until - // `Stream` itself is celled. - let this: *mut Self = core::hint::black_box(core::ptr::from_mut(self)); - // SAFETY: `this` is the live `&mut self` payload; no other `&` to - // `*this` exists between here and the dispatch call. - if let Some(last_frame_ref) = unsafe { (*this).data_frame_queue.peek_last() } { - // Raw, opaque-provenance pointer for post-dispatch accesses. - let last_frame: *mut PendingFrame = - core::hint::black_box(core::ptr::from_mut(last_frame_ref)); - // SAFETY: helper for the pre-dispatch accesses below; `last_frame` - // is the unique tail slot in `self.data_frame_queue.data`, valid - // until the dispatch call (after which we re-`black_box` before - // every access — see note above). - macro_rules! lf { - () => { - // SAFETY: `last_frame` points at the live tail slot of - // `self.data_frame_queue`; provenance is re-laundered via - // `black_box` before each post-dispatch expansion so no - // other `&mut` to the slot is live here (see note). - unsafe { &mut *last_frame } - }; - } + // `dispatch_write_callback()` re-enters JS and can reach `queue_frame()` again for this + // same stream, so every queue mutation stays inside the `with_mut` closure and the + // callback is dispatched only after that borrow has ended. + enum Merge { + Finished(StrongOptional), + Continue(usize), + Fresh, + } + let merged = self.data_frame_queue.with_mut(|queue| { + let Some(last) = queue.peek_last() else { + return Merge::Fresh; + }; if bytes.is_empty() { // just merge the end_stream - lf!().end_stream = end_stream; + last.end_stream = end_stream; // we can only hold 1 callback at a time so we conclude the last one, and keep the last one as pending // this is fine is like a per-stream CORKING in a frame level - let old_callback = core::mem::replace( - &mut lf!().callback, + return Merge::Finished(core::mem::replace( + &mut last.callback, StrongOptional::create(callback, &global_this), - ); + )); + } + if last.len == 0 { + // we have an empty frame with means we can just use this frame with a new buffer + last.buffer = vec![0u8; MAX_PAYLOAD_SIZE_WITHOUT_FRAME]; + } + let max_size = MAX_PAYLOAD_SIZE_WITHOUT_FRAME as u32; + let remaining = max_size - last.len; + if remaining == 0 { + return Merge::Fresh; + } + // ok we can cork frames + let consumed_len = (remaining as usize).min(bytes.len()); + let len = last.len as usize; + last.buffer[len..len + consumed_len].copy_from_slice(&bytes[0..consumed_len]); + last.len += u32::try_from(consumed_len).expect("int cast"); + bun_output::scoped_log!(H2FrameParser, "dataFrame merged {}", consumed_len); + + client + .queued_data_size + .set(client.queued_data_size.get() + consumed_len as u64); + // lets fallthrough if we still have some data + if consumed_len == bytes.len() { + last.end_stream = end_stream; + // we can only hold 1 callback at a time so we conclude the last one, and keep the last one as pending + // this is fine is like a per-stream CORKING in a frame level + return Merge::Finished(core::mem::replace( + &mut last.callback, + StrongOptional::create(callback, &global_this), + )); + } + Merge::Continue(consumed_len) + }); + match merged { + Merge::Finished(old_callback) => { if let Some(old_callback_value) = old_callback.get() { - // Escape `this` so a self-derived address is observable - // across the opaque JS call (belt-and-suspenders; either - // launder alone defeats the caching). - core::hint::black_box(this); client.dispatch_write_callback(old_callback_value); } drop(old_callback); return; } - if lf!().len == 0 { - // we have an empty frame with means we can just use this frame with a new buffer - lf!().buffer = vec![0u8; MAX_PAYLOAD_SIZE_WITHOUT_FRAME]; - } - let max_size = MAX_PAYLOAD_SIZE_WITHOUT_FRAME as u32; - let remaining = max_size - lf!().len; - if remaining > 0 { - // ok we can cork frames - let consumed_len = (remaining as usize).min(bytes.len()); - let merge = &bytes[0..consumed_len]; - let len = lf!().len as usize; - lf!().buffer[len..len + consumed_len].copy_from_slice(merge); - lf!().len += u32::try_from(consumed_len).expect("int cast"); - bun_output::scoped_log!(H2FrameParser, "dataFrame merged {}", consumed_len); - - client - .queued_data_size - .set(client.queued_data_size.get() + consumed_len as u64); - // lets fallthrough if we still have some data - let more_data = &bytes[consumed_len..]; - if more_data.is_empty() { - lf!().end_stream = end_stream; - // we can only hold 1 callback at a time so we conclude the last one, and keep the last one as pending - // this is fine is like a per-stream CORKING in a frame level - let old_callback = core::mem::replace( - &mut lf!().callback, - StrongOptional::create(callback, &global_this), - ); - if let Some(old_callback_value) = old_callback.get() { - core::hint::black_box(this); - client.dispatch_write_callback(old_callback_value); - } - drop(old_callback); - return; - } + Merge::Continue(consumed_len) => { // we keep the old callback because the new will be part of another frame - // SAFETY: `this` is the live `&mut self`; no borrow of `*this` - // is held here (the `last_frame` raw pointer is unused past - // this point). - return unsafe { (*this).queue_frame(client, more_data, callback, end_stream) }; + return self.queue_frame(client, &bytes[consumed_len..], callback, end_stream); } + Merge::Fresh => {} } bun_output::scoped_log!( H2FrameParser, @@ -2042,7 +2022,7 @@ impl Stream { global_this.vm().deprecated_report_extra_memory(bytes.len()); } bun_output::scoped_log!(H2FrameParser, "dataFrame enqueued {}", frame.len); - self.data_frame_queue.enqueue(frame); + self.data_frame_queue.with_mut(|q| q.enqueue(frame)); client .outbound_queue_size .set(client.outbound_queue_size.get() + 1); @@ -2059,27 +2039,27 @@ impl Stream { ) -> Stream { Stream { id: stream_identifier, - state: StreamState::OPEN, - js_context: StrongOptional::empty(), - wait_for_trailers: false, - end_after_headers: false, - is_waiting_more_headers: false, - header_block_size: 0, - header_block_count: 0, - pending_header_block: Vec::new(), - pending_header_flags: 0, - padding: None, - padding_strategy, - rst_code: 0, - stream_dependency: 0, - exclusive: false, - weight: 36, - window_size: initial_window_size as u64, - used_window_size: 0, - remote_window_size: remote_window_size as u64, - remote_used_window_size: 0, - signal: None, - data_frame_queue: PendingQueue::default(), + state: Cell::new(StreamState::OPEN), + js_context: JsCell::new(StrongOptional::empty()), + wait_for_trailers: Cell::new(false), + end_after_headers: Cell::new(false), + is_waiting_more_headers: Cell::new(false), + header_block_size: Cell::new(0), + header_block_count: Cell::new(0), + pending_header_block: JsCell::new(Vec::new()), + pending_header_flags: Cell::new(0), + padding: Cell::new(None), + padding_strategy: Cell::new(padding_strategy), + rst_code: Cell::new(0), + stream_dependency: Cell::new(0), + exclusive: Cell::new(false), + weight: Cell::new(36), + window_size: Cell::new(initial_window_size as u64), + used_window_size: Cell::new(0), + remote_window_size: Cell::new(remote_window_size as u64), + remote_used_window_size: Cell::new(0), + signal: JsCell::new(None), + data_frame_queue: JsCell::new(PendingQueue::default()), } } @@ -2091,33 +2071,34 @@ impl Stream { /// - CLOSED: stream is finished pub fn can_receive_data(&self) -> bool { matches!( - self.state, + self.state.get(), StreamState::IDLE | StreamState::OPEN | StreamState::HALF_CLOSED_LOCAL ) } pub fn can_send_data(&self) -> bool { matches!( - self.state, + self.state.get(), StreamState::IDLE | StreamState::OPEN | StreamState::HALF_CLOSED_REMOTE ) } - pub fn set_context(&mut self, value: JSValue, global_object: &JSGlobalObject) { - let old = core::mem::replace( - &mut self.js_context, - StrongOptional::create(value, global_object), - ); + pub fn set_context(&self, value: JSValue, global_object: &JSGlobalObject) { + // `replace` so the old Strong is dropped after the cell borrow ends. + let old = self + .js_context + .replace(StrongOptional::create(value, global_object)); drop(old); } pub fn get_identifier(&self) -> JSValue { self.js_context + .get() .get() .unwrap_or_else(|| JSValue::js_number(self.id as f64)) } - pub fn attach_signal(&mut self, parser: &H2FrameParser, signal: &mut AbortSignal) { + pub fn attach_signal(&self, parser: &H2FrameParser, signal: &mut AbortSignal) { // `ref_()` bumps the C++ intrusive refcount and returns the same live // `self` pointer with FFI (wildcard) provenance — store *that* in the // `BackRef` so its validity is tied to the refcount, not to the @@ -2134,23 +2115,26 @@ impl Stream { signal.listen(&raw mut *signal_ref); // TODO: We should not need this ref counting here, since Parser owns Stream parser.ref_(); - self.signal = Some(signal_ref); + // `replace` so any prior SignalRef drops (and unrefs the parser) outside the cell. + let old = self.signal.replace(Some(signal_ref)); + drop(old); } - pub fn detach_context(&mut self) { - self.js_context.deinit(); + pub fn detach_context(&self) { + self.js_context.with_mut(|ctx| ctx.deinit()); } - fn clean_queue(&mut self, client: &H2FrameParser) { + fn clean_queue(&self, client: &H2FrameParser) { bun_output::scoped_log!( H2FrameParser, "cleanQueue len: {} front: {} outboundQueueSize: {}", - self.data_frame_queue.len, - self.data_frame_queue.front, + self.data_frame_queue.get().len, + self.data_frame_queue.get().front, client.outbound_queue_size.get() ); - let mut queue = core::mem::take(&mut self.data_frame_queue); + // Take the queue out first: `dispatch_write_callback` below re-enters JS. + let mut queue = self.data_frame_queue.replace(PendingQueue::default()); while let Some(item) = queue.dequeue() { let frame = item; let len = frame.slice().len(); @@ -2172,7 +2156,7 @@ impl Stream { } /// this can be called multiple times - pub fn free_resources(&mut self, client: &H2FrameParser) { + pub fn free_resources(&self, client: &H2FrameParser) { // The rewrite engine only sees inbound traffic, so a completed request would leave // its engine entry as HalfClosedRemote and its legacy slot + Box behind forever — // one entry per request. Queue the id; the next rewrite_read batch evicts the engine @@ -2191,7 +2175,7 @@ impl Stream { } self.detach_context(); self.clean_queue::(client); - if let Some(signal) = self.signal.take() { + if let Some(signal) = self.signal.replace(None) { drop(signal); } // unsafe to ask GC to run if we are already inside GC @@ -2282,7 +2266,7 @@ impl H2FrameParser { /// Calculate the new window size for the connection and the stream /// https://datatracker.ietf.org/doc/html/rfc7540#section-6.9.1 - fn adjust_window_size(&self, stream: Option<&mut Stream>, payload_size: u32) { + fn adjust_window_size(&self, stream: Option<&Stream>, payload_size: u32) { self.used_window_size.set( self.used_window_size .get() @@ -2310,8 +2294,9 @@ impl H2FrameParser { } if let Some(s) = stream { - s.used_window_size += payload_size as u64; - if s.used_window_size > s.window_size { + s.used_window_size + .set(s.used_window_size.get() + payload_size as u64); + if s.used_window_size.get() > s.window_size.get() { // we are receiving more data than we are allowed to self.send_go_away( s.id, @@ -2320,7 +2305,8 @@ impl H2FrameParser { self.last_stream_id.get(), true, ); - s.used_window_size -= payload_size as u64; + s.used_window_size + .set(s.used_window_size.get() - payload_size as u64); } } } @@ -2330,23 +2316,25 @@ impl H2FrameParser { let mut updates: Vec<(u32, u64)> = Vec::new(); for (_, item) in self.streams.get().iter() { // SAFETY: item is &*mut Stream from streams.iter(); the boxed Stream outlives the iteration - let stream = unsafe { &mut **item }; + let stream = unsafe { &**item }; bun_output::scoped_log!( H2FrameParser, "incrementWindowSizeIfNeeded stream {} {} {} {}", stream.id, - stream.used_window_size, - stream.window_size, + stream.used_window_size.get(), + stream.window_size.get(), self.is_server.get() ); - if stream.used_window_size >= stream.window_size / 2 && stream.used_window_size > 0 { - let consumed = stream.used_window_size; - stream.used_window_size = 0; + if stream.used_window_size.get() >= stream.window_size.get() / 2 + && stream.used_window_size.get() > 0 + { + let consumed = stream.used_window_size.get(); + stream.used_window_size.set(0); bun_output::scoped_log!( H2FrameParser, "incrementWindowSizeIfNeeded stream {} {} {}", stream.id, - stream.window_size, + stream.window_size.get(), self.is_server.get() ); updates.push((stream.id, consumed)); @@ -2408,7 +2396,7 @@ impl H2FrameParser { true } - pub(crate) fn abort_stream(&self, stream: &mut Stream, abort_reason: JSValue) { + pub(crate) fn abort_stream(&self, stream: &Stream, abort_reason: JSValue) { bun_output::scoped_log!( H2FrameParser, "HTTP_FRAME_RST_STREAM id: {} code: CANCEL", @@ -2427,11 +2415,11 @@ impl H2FrameParser { }; let _ = frame.write(&mut writer_stream); let mut value: u32 = ErrorCode::CANCEL.0; - stream.rst_code = value; + stream.rst_code.set(value); value = value.swap_bytes(); let _ = writer_stream.write_all(&value.to_ne_bytes()); - let old_state = stream.state; - stream.state = StreamState::CLOSED; + let old_state = stream.state.get(); + stream.state.set(StreamState::CLOSED); let identifier = stream.get_identifier(); identifier.ensure_still_alive(); stream.free_resources::(self); @@ -2444,14 +2432,14 @@ impl H2FrameParser { let _ = self.write(&buffer); } - pub(crate) fn end_stream(&self, stream: &mut Stream, rst_code: ErrorCode) { + pub(crate) fn end_stream(&self, stream: &Stream, rst_code: ErrorCode) { bun_output::scoped_log!( H2FrameParser, "HTTP_FRAME_RST_STREAM id: {} code: {}", stream.id, rst_code.0 ); - if stream.state == StreamState::CLOSED { + if stream.state.get() == StreamState::CLOSED { return; } let mut buffer = [0u8; FrameHeader::BYTE_SIZE + 4]; @@ -2465,11 +2453,11 @@ impl H2FrameParser { }; let _ = frame.write(&mut writer_stream); let mut value: u32 = rst_code.0; - stream.rst_code = value; + stream.rst_code.set(value); value = value.swap_bytes(); let _ = writer_stream.write_all(&value.to_ne_bytes()); - stream.state = StreamState::CLOSED; + stream.state.set(StreamState::CLOSED); let identifier = stream.get_identifier(); identifier.ensure_still_alive(); stream.free_resources::(self); @@ -2477,7 +2465,7 @@ impl H2FrameParser { self.dispatch_with_extra( JSH2FrameParser::Gc::onStreamEnd, identifier, - JSValue::js_number(stream.state as u8 as f64), + JSValue::js_number(stream.state.get() as u8 as f64), ); } else { self.dispatch_with_extra( @@ -2688,14 +2676,14 @@ impl H2FrameParser { /// Reborrows a host fn's `*mut Stream` with the dispatch guard armed for the borrow's whole /// lifetime: user JS the caller runs while holding it (option getters, `toString`) can - /// re-enter `read()` without freeing the stream. Use this instead of a raw `&mut *ptr`. + /// re-enter `read()` without freeing the stream. Use this instead of a raw `&*ptr`. fn enter_stream_dispatch(&self, stream_ptr: *mut Stream) -> GuardedStream<'_> { let _dispatch = self.enter_dispatch(); GuardedStream { // SAFETY: stream_ptr is the heap::alloc'd *mut Stream stored in self.streams; the // map entry outlives the returned borrow because the armed dispatch depth defers // the only free path (rewrite_read's pending close drain) while the guard is live. - stream: unsafe { &mut *stream_ptr }, + stream: unsafe { &*stream_ptr }, _dispatch, } } @@ -2961,7 +2949,7 @@ impl H2FrameParser { while let Some(stream) = it.next() { // SAFETY: stream is a *mut Stream from self.streams (heap::alloc); valid while the // map entry exists. Separate heap allocation from `self`, so no aliasing. - let stream = unsafe { &mut *stream }; + let stream = unsafe { &*stream }; // reach backpressure let result = stream.flush_queue(self, &mut written); match result { @@ -3547,7 +3535,7 @@ impl H2FrameParser { if increment == 0 { if let Some(s) = stream { // SAFETY: s is *mut Stream from self.streams; valid while the map entry exists - self.end_stream(unsafe { &mut *s }, ErrorCode::PROTOCOL_ERROR); + self.end_stream(unsafe { &*s }, ErrorCode::PROTOCOL_ERROR); } else { self.send_go_away( 0, @@ -3563,14 +3551,13 @@ impl H2FrameParser { // FLOW_CONTROL_ERROR (stream-scoped on a stream, connection-scoped on stream 0). if let Some(s) = stream { // SAFETY: s is *mut Stream from self.streams; valid while the map entry exists - let next = unsafe { (*s).remote_window_size } + increment as u64; + let s = unsafe { &*s }; + let next = s.remote_window_size.get() + increment as u64; if next > MAX_WINDOW_SIZE as u64 { - // SAFETY: s is *mut Stream from self.streams; valid while the map entry exists - self.end_stream(unsafe { &mut *s }, ErrorCode::FLOW_CONTROL_ERROR); + self.end_stream(s, ErrorCode::FLOW_CONTROL_ERROR); return end; } - // SAFETY: s is *mut Stream from self.streams; valid while the map entry exists - unsafe { (*s).remote_window_size = next }; + s.remote_window_size.set(next); } else if frame.stream_identifier == 0 { let next = self.remote_window_size.get() + increment as u64; if next > MAX_WINDOW_SIZE as u64 { @@ -3721,7 +3708,7 @@ impl H2FrameParser { pub(crate) fn decode_header_block( &self, payload: &[u8], - stream: &mut Stream, + stream: &Stream, flags: u8, ) -> JsResult> { bun_output::scoped_log!( @@ -3785,15 +3772,21 @@ impl H2FrameParser { // RFC 7540 Section 6.5.2: Calculate header list size // Size = name length + value length + HPACK entry overhead per header - stream.header_block_size += - header.name.len() + header.value.len() + HPACK_ENTRY_OVERHEAD; - stream.header_block_count += 1; + stream.header_block_size.set( + stream.header_block_size.get() + + header.name.len() + + header.value.len() + + HPACK_ENTRY_OVERHEAD, + ); + stream + .header_block_count + .set(stream.header_block_count.get() + 1); // Check against maxHeaderListSize / maxHeaderListPairs. if rejected - || stream.header_block_size + || stream.header_block_size.get() > self.local_settings.get().max_header_list_size as usize - || (self.max_header_list_pairs.get() as usize) < stream.header_block_count + || (self.max_header_list_pairs.get() as usize) < stream.header_block_count.get() { rejected = true; continue; @@ -3910,8 +3903,8 @@ impl H2FrameParser { ); return data.len(); }; - // SAFETY: stream_ptr is a *mut Stream stored in self.streams (heap::alloc); valid for the lifetime of the entry, exclusive access reshaped for borrowck - let mut stream = unsafe { &mut *stream_ptr }; + // SAFETY: stream_ptr is a *mut Stream stored in self.streams (heap::alloc); valid while the map entry exists + let mut stream = unsafe { &*stream_ptr }; let max_frame_size = self.local_settings.get().max_frame_size; if frame.length > max_frame_size { @@ -3935,8 +3928,6 @@ impl H2FrameParser { let mut payload = &data[0..end]; // window size considering the full frame.length received so far self.adjust_window_size(Some(stream), payload.len() as u32); - // SAFETY: stream_ptr unchanged; re-borrow after intervening call (borrowck reshape) - stream = unsafe { &mut *stream_ptr }; let previous_remaining_length: isize = self.remaining_length.get() as isize; self.remaining_length @@ -3955,7 +3946,7 @@ impl H2FrameParser { ); return data.len(); } - if let Some(p) = stream.padding { + if let Some(p) = stream.padding.get() { padding = p; } else { if payload.is_empty() { @@ -3963,7 +3954,7 @@ impl H2FrameParser { return data.len(); } padding = payload[0]; - stream.padding = Some(payload[0]); + stream.padding.set(Some(payload[0])); } // RFC 7540 Section 6.1: If the length of the padding is the length of // the frame payload or greater, the recipient MUST treat this as a @@ -4034,11 +4025,11 @@ impl H2FrameParser { } if self.remaining_length.get() == 0 { self.current_frame.set(None); - stream.padding = None; + stream.padding.set(None); if emitted { stream = match self.streams.get().get(&frame.stream_identifier).copied() { // SAFETY: s is *mut Stream from self.streams (heap::alloc); valid while the map entry exists - Some(s) => unsafe { &mut *s }, + Some(s) => unsafe { &*s }, None => return end, }; } @@ -4046,16 +4037,16 @@ impl H2FrameParser { let identifier = stream.get_identifier(); identifier.ensure_still_alive(); - if stream.state == StreamState::HALF_CLOSED_LOCAL { - stream.state = StreamState::CLOSED; + if stream.state.get() == StreamState::HALF_CLOSED_LOCAL { + stream.state.set(StreamState::CLOSED); stream.free_resources::(self); } else { - stream.state = StreamState::HALF_CLOSED_REMOTE; + stream.state.set(StreamState::HALF_CLOSED_REMOTE); } self.dispatch_with_extra( JSH2FrameParser::Gc::onStreamEnd, identifier, - JSValue::js_number(stream.state as u8 as f64), + JSValue::js_number(stream.state.get() as u8 as f64), ); } } @@ -4295,8 +4286,8 @@ impl H2FrameParser { ); return data.len(); }; - // SAFETY: stream_ptr is a *mut Stream stored in self.streams (heap::alloc); valid for the lifetime of the entry, exclusive access reshaped for borrowck - let stream = unsafe { &mut *stream_ptr }; + // SAFETY: stream_ptr is a *mut Stream stored in self.streams (heap::alloc); valid while the map entry exists + let stream = unsafe { &*stream_ptr }; if frame.length != 4 { self.send_go_away( @@ -4309,7 +4300,7 @@ impl H2FrameParser { return data.len(); } - if stream.is_waiting_more_headers { + if stream.is_waiting_more_headers.get() { self.send_go_away( frame.stream_identifier, ErrorCode::PROTOCOL_ERROR, @@ -4323,10 +4314,10 @@ impl H2FrameParser { if let Some(content) = self.handle_incomming_payload(data, frame.stream_identifier) { let payload = content.data(); let rst_code = u32_from_bytes(payload); - stream.rst_code = rst_code; + stream.rst_code.set(rst_code); let end = content.end; self.read_buffer.with_mut(|rb| rb.reset()); - stream.state = StreamState::CLOSED; + stream.state.set(StreamState::CLOSED); let identifier = stream.get_identifier(); identifier.ensure_still_alive(); stream.free_resources::(self); @@ -4334,7 +4325,7 @@ impl H2FrameParser { self.dispatch_with_extra( JSH2FrameParser::Gc::onStreamEnd, identifier, - JSValue::js_number(stream.state as u8 as f64), + JSValue::js_number(stream.state.get() as u8 as f64), ); } else { self.dispatch_with_extra( @@ -4457,8 +4448,8 @@ impl H2FrameParser { ); return data.len(); }; - // SAFETY: stream_ptr is a *mut Stream stored in self.streams (heap::alloc); valid for the lifetime of the entry, exclusive access reshaped for borrowck - let stream = unsafe { &mut *stream_ptr }; + // SAFETY: stream_ptr is a *mut Stream stored in self.streams (heap::alloc); valid while the map entry exists + let stream = unsafe { &*stream_ptr }; if let Some(content) = self.handle_incomming_payload(data, frame.stream_identifier) { let payload = content.data(); @@ -4479,9 +4470,9 @@ impl H2FrameParser { ); return end; } - stream.stream_dependency = stream_identifier.uint31(); - stream.exclusive = stream_identifier.reserved(); - stream.weight = priority.weight as u16; + stream.stream_dependency.set(stream_identifier.uint31()); + stream.exclusive.set(stream_identifier.reserved()); + stream.weight.set(priority.weight as u16); return end; } @@ -4506,10 +4497,10 @@ impl H2FrameParser { ); return Ok(data.len()); }; - // SAFETY: stream_ptr is a *mut Stream stored in self.streams (heap::alloc); valid for the lifetime of the entry, exclusive access reshaped for borrowck - let mut stream = unsafe { &mut *stream_ptr }; + // SAFETY: stream_ptr is a *mut Stream stored in self.streams (heap::alloc); valid while the map entry exists + let mut stream = unsafe { &*stream_ptr }; - if !stream.is_waiting_more_headers { + if !stream.is_waiting_more_headers.get() { self.send_go_away( frame.stream_identifier, ErrorCode::PROTOCOL_ERROR, @@ -4533,7 +4524,7 @@ impl H2FrameParser { let payload = content.data(); let end = content.end; self.read_buffer.with_mut(|rb| rb.reset()); - if stream.pending_header_block.len() + payload.len() + if stream.pending_header_block.get().len() + payload.len() > self.local_settings.get().max_header_list_size as usize { // Cap the buffered compressed block at max_header_list_size as a @@ -4548,27 +4539,30 @@ impl H2FrameParser { ); return Ok(end); } - stream.pending_header_block.extend_from_slice(payload); + stream + .pending_header_block + .with_mut(|b| b.extend_from_slice(payload)); if frame.flags & HeadersFrameFlags::END_HEADERS as u8 == 0 { // keep buffering until END_HEADERS arrives return Ok(end); } - stream.is_waiting_more_headers = false; + stream.is_waiting_more_headers.set(false); self.expecting_continuation.set(0); // Take ownership of the buffer so re-entrant parser calls from the // onStreamHeaders dispatch can't alias or free the bytes being decoded. - let block = core::mem::take(&mut stream.pending_header_block); + let block = stream.pending_header_block.replace(Vec::new()); // Report the original HEADERS frame's flags (plus END_HEADERS now // that the block is complete), not the CONTINUATION frame's. - let block_flags = stream.pending_header_flags | HeadersFrameFlags::END_HEADERS as u8; + let block_flags = + stream.pending_header_flags.get() | HeadersFrameFlags::END_HEADERS as u8; stream = match self.decode_header_block(&block, stream, block_flags)? { // SAFETY: s is *mut Stream from self.streams (heap::alloc); valid while the map entry exists - Some(s) => unsafe { &mut *s }, + Some(s) => unsafe { &*s }, None => return Ok(end), }; // END_STREAM finalization was deferred by handle_headers_frame // until the complete header block had been dispatched. - if stream.end_after_headers { + if stream.end_after_headers.get() { self.finish_headers_end_stream(stream); } return Ok(end); @@ -4580,28 +4574,28 @@ impl H2FrameParser { /// Finalize a stream whose HEADERS frame carried END_STREAM, after the /// complete header block has been decoded and dispatched. - fn finish_headers_end_stream(&self, stream: &mut Stream) { + fn finish_headers_end_stream(&self, stream: &Stream) { // The stream can be reset (req.close(), AbortSignal) between the // HEADERS fragment and the CONTINUATION that completes the block; // don't regress a CLOSED stream or dispatch onStreamEnd after // onStreamError. - if stream.state == StreamState::CLOSED { + if stream.state.get() == StreamState::CLOSED { return; } let identifier = stream.get_identifier(); identifier.ensure_still_alive(); // no more continuation headers we can call it closed - if stream.state == StreamState::HALF_CLOSED_LOCAL { - stream.state = StreamState::CLOSED; + if stream.state.get() == StreamState::HALF_CLOSED_LOCAL { + stream.state.set(StreamState::CLOSED); stream.free_resources::(self); } else { - stream.state = StreamState::HALF_CLOSED_REMOTE; + stream.state.set(StreamState::HALF_CLOSED_REMOTE); } self.dispatch_with_extra( JSH2FrameParser::Gc::onStreamEnd, identifier, - JSValue::js_number(stream.state as u8 as f64), + JSValue::js_number(stream.state.get() as u8 as f64), ); } @@ -4630,8 +4624,8 @@ impl H2FrameParser { ); return Ok(data.len()); }; - // SAFETY: stream_ptr is a *mut Stream stored in self.streams (heap::alloc); valid for the lifetime of the entry, exclusive access reshaped for borrowck - let mut stream = unsafe { &mut *stream_ptr }; + // SAFETY: stream_ptr is a *mut Stream stored in self.streams (heap::alloc); valid while the map entry exists + let mut stream = unsafe { &*stream_ptr }; if frame.length > self.local_settings.get().max_frame_size { self.send_go_away( @@ -4644,7 +4638,7 @@ impl H2FrameParser { return Ok(data.len()); } - if stream.is_waiting_more_headers { + if stream.is_waiting_more_headers.get() { self.send_go_away( frame.stream_identifier, ErrorCode::PROTOCOL_ERROR, @@ -4702,13 +4696,16 @@ impl H2FrameParser { return Ok(end_); } let end = payload.len() - padding; - stream.end_after_headers = frame.flags & HeadersFrameFlags::END_STREAM as u8 != 0; - stream.header_block_size = 0; - stream.header_block_count = 0; - stream.pending_header_block.clear(); - stream.is_waiting_more_headers = - frame.flags & HeadersFrameFlags::END_HEADERS as u8 == 0; - if stream.is_waiting_more_headers { + stream + .end_after_headers + .set(frame.flags & HeadersFrameFlags::END_STREAM as u8 != 0); + stream.header_block_size.set(0); + stream.header_block_count.set(0); + stream.pending_header_block.with_mut(|b| b.clear()); + stream + .is_waiting_more_headers + .set(frame.flags & HeadersFrameFlags::END_HEADERS as u8 == 0); + if stream.is_waiting_more_headers.get() { // Buffer fragments until END_HEADERS (RFC 9113 §4.3); the block is // decoded and END_STREAM finalized in handle_continuation_frame so // the JS event order stays onStreamHeaders -> onStreamEnd. @@ -4726,17 +4723,19 @@ impl H2FrameParser { ); return Ok(end_); } - stream.pending_header_block.extend_from_slice(fragment); - stream.pending_header_flags = frame.flags; + stream + .pending_header_block + .with_mut(|b| b.extend_from_slice(fragment)); + stream.pending_header_flags.set(frame.flags); self.expecting_continuation.set(frame.stream_identifier); return Ok(end_); } stream = match self.decode_header_block(&payload[offset..end], stream, frame.flags)? { // SAFETY: s is *mut Stream from self.streams (heap::alloc); valid while the map entry exists - Some(s) => unsafe { &mut *s }, + Some(s) => unsafe { &*s }, None => return Ok(end_), }; - if stream.end_after_headers { + if stream.end_after_headers.get() { self.finish_headers_end_stream(stream); } return Ok(end_); @@ -4809,15 +4808,21 @@ impl H2FrameParser { let delta = new_size - old_size; for (_, item) in self.streams.get().iter() { // SAFETY: item is &*mut Stream from streams.iter(); the boxed Stream outlives the iteration - let stream = unsafe { &mut **item }; + let stream = unsafe { &**item }; if delta >= 0 { - stream.window_size = stream - .window_size - .saturating_add(u64::try_from(delta).expect("int cast")); + stream.window_size.set( + stream + .window_size + .get() + .saturating_add(u64::try_from(delta).expect("int cast")), + ); } else { - stream.window_size = stream - .window_size - .saturating_sub(u64::try_from(-delta).expect("int cast")); + stream.window_size.set( + stream + .window_size + .get() + .saturating_sub(u64::try_from(-delta).expect("int cast")), + ); } } bun_output::scoped_log!( @@ -4857,12 +4862,13 @@ impl H2FrameParser { if remote_settings.initial_window_size as u64 >= self.remote_window_size.get() { for (_, item) in self.streams.get().iter() { // SAFETY: item is &*mut Stream from streams.iter(); the boxed Stream outlives the iteration - let stream = unsafe { &mut **item }; + let stream = unsafe { &**item }; if remote_settings.initial_window_size as u64 - >= stream.remote_window_size + >= stream.remote_window_size.get() { - stream.remote_window_size = - remote_settings.initial_window_size as u64; + stream + .remote_window_size + .set(remote_settings.initial_window_size as u64); } } } @@ -4959,9 +4965,12 @@ impl H2FrameParser { if remote_settings.initial_window_size as u64 >= self.remote_window_size.get() { for (_, item) in self.streams.get().iter() { // SAFETY: item is &*mut Stream from streams.iter(); the boxed Stream outlives the iteration - let stream = unsafe { &mut **item }; - if remote_settings.initial_window_size as u64 >= stream.remote_window_size { - stream.remote_window_size = remote_settings.initial_window_size as u64; + let stream = unsafe { &**item }; + if remote_settings.initial_window_size as u64 >= stream.remote_window_size.get() + { + stream + .remote_window_size + .set(remote_settings.initial_window_size as u64); } } } @@ -5606,9 +5615,11 @@ impl crate::api::h2::connection::Sink for H2FrameParser { // typically sent before the server's SETTINGS lands), then resume queued sends. for (_, item) in self.streams.get().iter() { // SAFETY: item is &*mut Stream from streams.iter(); the boxed Stream outlives the iteration - let stream = unsafe { &mut **item }; - if settings.initial_window_size as u64 >= stream.remote_window_size { - stream.remote_window_size = settings.initial_window_size as u64; + let stream = unsafe { &**item }; + if settings.initial_window_size as u64 >= stream.remote_window_size.get() { + stream + .remote_window_size + .set(settings.initial_window_size as u64); } } let _ = self.flush(); @@ -5665,7 +5676,10 @@ impl crate::api::h2::connection::Sink for H2FrameParser { .set(self.remote_window_size.get() + increment as u64); } else if let Some(stream) = self.streams.get().get(&stream_id).copied() { // SAFETY: stream is *mut Stream from self.streams; valid while the map entry exists - unsafe { (*stream).remote_window_size += increment as u64 }; + let stream = unsafe { &*stream }; + stream + .remote_window_size + .set(stream.remote_window_size.get() + increment as u64); } let _ = self.flush(); } @@ -5769,7 +5783,7 @@ impl crate::api::h2::connection::Sink for H2FrameParser { // Bridge: the JS endAfterHeaders getter reads the legacy stream's end_after_headers flag. if let Some(stream) = self.streams.get().get(&stream_id).copied() { // SAFETY: stream is *mut Stream from self.streams; valid while the map entry exists - unsafe { (*stream).end_after_headers = end_stream }; + unsafe { &*stream }.end_after_headers.set(end_stream); } // Materialize the accumulated block in a single native pass: the raw array, the // node-shaped headers object, and the sensitive list (a zero-field block yields an @@ -5837,7 +5851,8 @@ impl crate::api::h2::connection::Sink for H2FrameParser { let mut effective = state; if let Some(stream) = self.streams.get().get(&stream_id).copied() { // SAFETY: stream is *mut Stream from self.streams; valid while the map entry exists - let legacy_state = unsafe { (*stream).state }; + let stream = unsafe { &*stream }; + let legacy_state = stream.state.get(); if state == 6 && matches!( legacy_state, @@ -5846,15 +5861,12 @@ impl crate::api::h2::connection::Sink for H2FrameParser { { effective = 7; } - // SAFETY: stream is *mut Stream from self.streams; valid while the map entry exists - unsafe { - (*stream).state = match effective { - 5 => StreamState::HALF_CLOSED_LOCAL, - 6 => StreamState::HALF_CLOSED_REMOTE, - 7 => StreamState::CLOSED, - _ => legacy_state, - }; - } + stream.state.set(match effective { + 5 => StreamState::HALF_CLOSED_LOCAL, + 6 => StreamState::HALF_CLOSED_REMOTE, + 7 => StreamState::CLOSED, + _ => legacy_state, + }); } let stream_ctx = self.rewrite_stream_ctx(stream_id); self.dispatch_with_extra( @@ -5909,11 +5921,10 @@ impl crate::api::h2::connection::Sink for H2FrameParser { let mut old_state: u8 = StreamState::OPEN as u8; if let Some(stream) = self.streams.get().get(&stream_id).copied() { // SAFETY: stream is *mut Stream from self.streams; valid while the map entry exists - unsafe { - old_state = (*stream).state as u8; - (*stream).state = StreamState::CLOSED; - (*stream).rst_code = code; - } + let stream = unsafe { &*stream }; + old_state = stream.state.get() as u8; + stream.state.set(StreamState::CLOSED); + stream.rst_code.set(code); } let stream_ctx = self.rewrite_stream_ctx(stream_id); if code == crate::api::h2::wire::ErrorCode::Cancel.as_u32() { @@ -6225,11 +6236,11 @@ impl H2FrameParser { } for (_, item) in this.streams.get().iter() { // SAFETY: item is &*mut Stream from streams.iter(); the boxed Stream outlives the iteration - let stream = unsafe { &mut **item }; - if stream.used_window_size > window_size_value as u64 { + let stream = unsafe { &**item }; + if stream.used_window_size.get() > window_size_value as u64 { continue; } - stream.window_size = window_size_value as u64; + stream.window_size.set(window_size_value as u64); } Ok(JSValue::UNDEFINED) } @@ -6562,7 +6573,7 @@ impl H2FrameParser { }; // SAFETY: stream is *mut Stream from self.streams; valid while the map entry exists - Ok(JSValue::from(unsafe { (*stream).end_after_headers })) + Ok(JSValue::from(unsafe { &*stream }.end_after_headers.get())) } #[bun_jsc::host_fn(method)] @@ -6592,12 +6603,13 @@ impl H2FrameParser { // SAFETY: stream is a *mut Stream from self.streams (heap::alloc); valid while the map entry exists let stream = unsafe { &*stream }; - if let Some(signal_ref) = &stream.signal { + if let Some(signal_ref) = stream.signal.get() { return Ok(JSValue::from(signal_ref.is_aborted())); } // closed with cancel = aborted Ok(JSValue::from( - stream.state == StreamState::CLOSED && stream.rst_code == ErrorCode::CANCEL.0, + stream.state.get() == StreamState::CLOSED + && stream.rst_code.get() == ErrorCode::CANCEL.0, )) } @@ -6626,18 +6638,18 @@ impl H2FrameParser { return Err(global_object.throw(format_args!("Invalid stream id"))); }; // SAFETY: stream is a *mut Stream from self.streams (heap::alloc); valid while the map entry exists - let stream = unsafe { &mut *stream }; + let stream = unsafe { &*stream }; let state = JSValue::create_empty_object(global_object, 6); state.put( global_object, b"localWindowSize", - JSValue::js_number(stream.window_size as f64), + JSValue::js_number(stream.window_size.get() as f64), ); state.put( global_object, b"state", - JSValue::js_number(stream.state as u8 as f64), + JSValue::js_number(stream.state.get() as u8 as f64), ); state.put( global_object, @@ -6658,7 +6670,7 @@ impl H2FrameParser { state.put( global_object, b"weight", - JSValue::js_number(stream.weight as f64), + JSValue::js_number(stream.weight.get() as f64), ); Ok(state) @@ -6690,7 +6702,7 @@ impl H2FrameParser { return Err(global_object.throw(format_args!("Invalid stream id"))); }; // The `options` getters below can run user JS while `stream` is borrowed. - let mut stream = this.enter_stream_dispatch(stream_ptr); + let stream = this.enter_stream_dispatch(stream_ptr); if !stream.can_send_data() && !stream.can_receive_data() { return Ok(JSValue::FALSE); @@ -6700,9 +6712,9 @@ impl H2FrameParser { return Err(global_object.throw(format_args!("Invalid priority"))); } - let mut weight = stream.weight; - let mut exclusive = stream.exclusive; - let mut parent_id = stream.stream_dependency; + let mut weight = stream.weight.get(); + let mut exclusive = stream.exclusive.get(); + let mut parent_id = stream.stream_dependency.get(); let mut silent = false; if let Some(js_weight) = options.get(global_object, "weight")? { if js_weight.is_number() { @@ -6750,17 +6762,17 @@ impl H2FrameParser { return Ok(JSValue::FALSE); } - stream.stream_dependency = parent_id; - stream.exclusive = exclusive; - stream.weight = weight; + stream.stream_dependency.set(parent_id); + stream.exclusive.set(exclusive); + stream.weight.set(weight); if !silent { let stream_identifier = - UInt31WithReserved::init(stream.stream_dependency, stream.exclusive); + UInt31WithReserved::init(stream.stream_dependency.get(), stream.exclusive.get()); let priority = StreamPriority { stream_identifier: stream_identifier.to_uint32(), - weight: stream.weight as u8, + weight: stream.weight.get() as u8, }; let frame = FrameHeader { type_: FrameType::HTTP_FRAME_PRIORITY as u8, @@ -6846,7 +6858,7 @@ impl H2FrameParser { }; // SAFETY: stream is a *mut Stream from self.streams; valid while the map entry exists - this.end_stream(unsafe { &mut *stream }, ErrorCode(error_code)); + this.end_stream(unsafe { &*stream }, ErrorCode(error_code)); Ok(JSValue::TRUE) } @@ -6886,7 +6898,7 @@ impl H2FrameParser { /// return value instead of re-entering the VM mid-host-call. fn send_data( &self, - stream: &mut Stream, + stream: &Stream, payload: &[u8], close: bool, callback: JSValue, @@ -6909,7 +6921,7 @@ impl H2FrameParser { let mut enqueued = false; self.ref_(); - let can_close = close && !stream.wait_for_trailers; + let can_close = close && !stream.wait_for_trailers.get(); if payload.is_empty() { // empty payload we still need to send a frame let data_header = FrameHeader { @@ -6945,7 +6957,8 @@ impl H2FrameParser { .min( (stream .remote_window_size - .saturating_sub(stream.remote_used_window_size)) + .get() + .saturating_sub(stream.remote_used_window_size.get())) as usize, ); let mut is_flow_control_limited = false; @@ -6996,7 +7009,9 @@ impl H2FrameParser { max_size, payload_size ); - stream.remote_used_window_size += payload_size as u64; + stream + .remote_used_window_size + .set(stream.remote_used_window_size.get() + payload_size as u64); self.remote_used_window_size .set(self.remote_used_window_size.get() + payload_size as u64); self.note_engine_send_consumed(stream_id, payload_size as u64); @@ -7104,25 +7119,25 @@ impl H2FrameParser { if !enqueued { self.dispatch_write_callback(callback); if close { - if stream.wait_for_trailers { + if stream.wait_for_trailers.get() { self.dispatch(JSH2FrameParser::Gc::onWantTrailers, stream.get_identifier()); } else { let identifier = stream.get_identifier(); identifier.ensure_still_alive(); - if stream.state == StreamState::HALF_CLOSED_REMOTE { - stream.state = StreamState::CLOSED; + if stream.state.get() == StreamState::HALF_CLOSED_REMOTE { + stream.state.set(StreamState::CLOSED); stream.free_resources::(self); } else { - stream.state = StreamState::HALF_CLOSED_LOCAL; + stream.state.set(StreamState::HALF_CLOSED_LOCAL); } - settled_state = stream.state as u8; + settled_state = stream.state.get() as u8; if !(suppress_half_closed_local_dispatch - && stream.state == StreamState::HALF_CLOSED_LOCAL) + && stream.state.get() == StreamState::HALF_CLOSED_LOCAL) { self.dispatch_with_extra( JSH2FrameParser::Gc::onStreamEnd, identifier, - JSValue::js_number(stream.state as u8 as f64), + JSValue::js_number(stream.state.get() as u8 as f64), ); } } @@ -7160,9 +7175,9 @@ impl H2FrameParser { return Err(global_object.throw(format_args!("Invalid stream id"))); }; // SAFETY: stream is a *mut Stream from self.streams (heap::alloc); valid while the map entry exists - let stream = unsafe { &mut *stream }; + let stream = unsafe { &*stream }; - stream.wait_for_trailers = false; + stream.wait_for_trailers.set(false); let _ = this.send_data(stream, b"", true, JSValue::UNDEFINED, false); Ok(JSValue::UNDEFINED) } @@ -7276,7 +7291,7 @@ impl H2FrameParser { }; // The header/sensitive-object getters and value coercions below can run user JS // while `stream` is borrowed. - let mut stream = this.enter_stream_dispatch(stream_ptr); + let stream = this.enter_stream_dispatch(stream_ptr); let Some(headers_obj) = headers_arg.get_object() else { return Err(global_object.throw(format_args!("Expected headers to be an object"))); @@ -7391,7 +7406,7 @@ impl H2FrameParser { // session down gracefully — the encoder state is no longer trustworthy // (node/nghttp2 treat this as fatal and close with a NO_ERROR GOAWAY). let triggering_id = stream.id; - this.end_stream(&mut stream, ErrorCode::FRAME_SIZE_ERROR); + this.end_stream(&stream, ErrorCode::FRAME_SIZE_ERROR); this.send_go_away( triggering_id, ErrorCode::NO_ERROR, @@ -7589,16 +7604,16 @@ impl H2FrameParser { } let identifier = stream.get_identifier(); identifier.ensure_still_alive(); - if stream.state == StreamState::HALF_CLOSED_REMOTE { - stream.state = StreamState::CLOSED; + if stream.state.get() == StreamState::HALF_CLOSED_REMOTE { + stream.state.set(StreamState::CLOSED); stream.free_resources::(this); } else { - stream.state = StreamState::HALF_CLOSED_LOCAL; + stream.state.set(StreamState::HALF_CLOSED_LOCAL); } this.dispatch_with_extra( JSH2FrameParser::Gc::onStreamEnd, identifier, - JSValue::js_number(stream.state as u8 as f64), + JSValue::js_number(stream.state.get() as u8 as f64), ); Ok(JSValue::UNDEFINED) } @@ -7627,7 +7642,7 @@ impl H2FrameParser { }; // Coercing `data_arg` (a String subclass's toString) can run user JS while `stream` // is borrowed. - let mut stream = this.enter_stream_dispatch(stream_ptr); + let stream = this.enter_stream_dispatch(stream_ptr); if !stream.can_send_data() { this.dispatch_write_callback(callback_arg); return Ok(JSValue::FALSE); @@ -7668,7 +7683,7 @@ impl H2FrameParser { } }; - let settled_state = this.send_data(&mut stream, buffer.slice(), close, callback_arg, true); + let settled_state = this.send_data(&stream, buffer.slice(), close, callback_arg, true); // 5 = HALF_CLOSED_LOCAL: the JS caller runs markWritableDone itself instead of // the engine re-entering the VM with an onStreamEnd(5) dispatch. @@ -7968,7 +7983,11 @@ impl H2FrameParser { }; // SAFETY: stream is *mut Stream from self.streams; valid while the map entry exists - Ok(unsafe { (*stream).js_context.get() }.unwrap_or(JSValue::UNDEFINED)) + Ok(unsafe { &*stream } + .js_context + .get() + .get() + .unwrap_or(JSValue::UNDEFINED)) } #[bun_jsc::host_fn(method)] @@ -8039,7 +8058,7 @@ impl H2FrameParser { let mut it = StreamResumableIterator::init(this); while let Some(stream) = it.next() { // SAFETY: stream is *mut Stream from self.streams; valid while the map entry exists - let Some(value) = (unsafe { (*stream).js_context.get() }) else { + let Some(value) = unsafe { &*stream }.js_context.get().get() else { continue; }; this.handlers.get().vm.event_loop_mut().run_callback( @@ -8065,7 +8084,7 @@ impl H2FrameParser { while let Some(stream_ptr) = it.next() { // SAFETY: stream_ptr is a *mut Stream stored in self.streams (heap::alloc); valid for // the lifetime of the entry. Separate heap allocation from `this`, so no aliasing. - let stream = unsafe { &mut *stream_ptr }; + let stream = unsafe { &*stream_ptr }; // this is the oposite logic of emitErrorToallStreams, in this case we wanna to cancel this streams if this.is_server.get() { if stream.id % 2 == 0 { @@ -8074,10 +8093,10 @@ impl H2FrameParser { } else if stream.id % 2 != 0 { continue; } - if stream.state != StreamState::CLOSED { - let old_state = stream.state; - stream.state = StreamState::CLOSED; - stream.rst_code = ErrorCode::CANCEL.0; + if stream.state.get() != StreamState::CLOSED { + let old_state = stream.state.get(); + stream.state.set(StreamState::CLOSED); + stream.rst_code.set(ErrorCode::CANCEL.0); let identifier = stream.get_identifier(); identifier.ensure_still_alive(); stream.free_resources::(this); @@ -8117,10 +8136,10 @@ impl H2FrameParser { while let Some(stream_ptr) = it.next() { // SAFETY: stream_ptr is a *mut Stream stored in self.streams (heap::alloc); valid for // the lifetime of the entry. Separate heap allocation from `this`, so no aliasing. - let stream = unsafe { &mut *stream_ptr }; - if stream.state != StreamState::CLOSED { - stream.state = StreamState::CLOSED; - stream.rst_code = rst_code; + let stream = unsafe { &*stream_ptr }; + if stream.state.get() != StreamState::CLOSED { + stream.state.set(StreamState::CLOSED); + stream.rst_code.set(rst_code); let identifier = stream.get_identifier(); identifier.ensure_still_alive(); stream.free_resources::(this); @@ -8318,18 +8337,18 @@ impl H2FrameParser { return Ok(JSValue::js_number(-1.0)); }; // SAFETY: stream is a *mut Stream from self.streams (heap::alloc); valid while the map entry exists - let stream = unsafe { &mut *stream }; - stream.state = StreamState::CLOSED; + let stream = unsafe { &*stream }; + stream.state.set(StreamState::CLOSED); if !stream_ctx_arg.is_empty_or_undefined_or_null() && stream_ctx_arg.is_object() { stream.set_context(stream_ctx_arg, global_object); } - stream.rst_code = ErrorCode::COMPRESSION_ERROR.0; + stream.rst_code.set(ErrorCode::COMPRESSION_ERROR.0); this.dispatch_with_extra( JSH2FrameParser::Gc::onStreamError, stream.get_identifier(), - JSValue::js_number(stream.rst_code as f64), + JSValue::js_number(stream.rst_code.get() as f64), ); return Ok(JSValue::js_number(stream_id as f64)); } @@ -8505,18 +8524,18 @@ impl H2FrameParser { return Ok(JSValue::js_number(-1.0)); }; // SAFETY: stream is a *mut Stream from self.streams (heap::alloc); valid while the map entry exists - let stream = unsafe { &mut *stream }; + let stream = unsafe { &*stream }; if !stream_ctx_arg.is_empty_or_undefined_or_null() && stream_ctx_arg.is_object() { stream.set_context(stream_ctx_arg, global_object); } - stream.state = StreamState::CLOSED; - stream.rst_code = ErrorCode::COMPRESSION_ERROR.0; + stream.state.set(StreamState::CLOSED); + stream.rst_code.set(ErrorCode::COMPRESSION_ERROR.0); this.dispatch_with_extra( JSH2FrameParser::Gc::onStreamError, stream.get_identifier(), - JSValue::js_number(stream.rst_code as f64), + JSValue::js_number(stream.rst_code.get() as f64), ); return Ok(JSValue::UNDEFINED); } @@ -8595,18 +8614,18 @@ impl H2FrameParser { return Ok(JSValue::js_number(-1.0)); }; // SAFETY: stream is a *mut Stream from self.streams (heap::alloc); valid while the map entry exists - let stream = unsafe { &mut *stream }; - stream.state = StreamState::CLOSED; + let stream = unsafe { &*stream }; + stream.state.set(StreamState::CLOSED); if !stream_ctx_arg.is_empty_or_undefined_or_null() && stream_ctx_arg.is_object() { stream.set_context(stream_ctx_arg, global_object); } - stream.rst_code = ErrorCode::COMPRESSION_ERROR.0; + stream.rst_code.set(ErrorCode::COMPRESSION_ERROR.0); this.dispatch_with_extra( JSH2FrameParser::Gc::onStreamError, stream.get_identifier(), - JSValue::js_number(stream.rst_code as f64), + JSValue::js_number(stream.rst_code.get() as f64), ); return Ok(JSValue::js_number(stream_id as f64)); } @@ -8619,7 +8638,7 @@ impl H2FrameParser { return Ok(JSValue::js_number(-1.0)); }; // The `options` getters below can run user JS while `stream` is borrowed. - let mut stream = this.enter_stream_dispatch(stream_ptr); + let stream = this.enter_stream_dispatch(stream_ptr); if !stream_ctx_arg.is_empty_or_undefined_or_null() && stream_ctx_arg.is_object() { stream.set_context(stream_ctx_arg, global_object); } @@ -8634,30 +8653,30 @@ impl H2FrameParser { if args_list.len > 4 && !args_list.ptr[4].is_empty_or_undefined_or_null() { let options = args_list.ptr[4]; if !options.is_object() { - stream.state = StreamState::CLOSED; - stream.rst_code = ErrorCode::INTERNAL_ERROR.0; + stream.state.set(StreamState::CLOSED); + stream.rst_code.set(ErrorCode::INTERNAL_ERROR.0); this.dispatch_with_extra( JSH2FrameParser::Gc::onStreamError, stream.get_identifier(), - JSValue::js_number(stream.rst_code as f64), + JSValue::js_number(stream.rst_code.get() as f64), ); return Ok(JSValue::js_number(stream_id as f64)); } if let Some(padding_js) = options.get(global_object, "paddingStrategy")? { if padding_js.is_number() { - stream.padding_strategy = match padding_js.to_u32() { + stream.padding_strategy.set(match padding_js.to_u32() { 1 => PaddingStrategy::Aligned, 2 => PaddingStrategy::Max, _ => PaddingStrategy::None, - }; + }); } } if let Some(trailes_js) = options.get(global_object, "waitForTrailers")? { if trailes_js.is_boolean() { wait_for_trailers = trailes_js.as_boolean(); - stream.wait_for_trailers = wait_for_trailers; + stream.wait_for_trailers.set(wait_for_trailers); } } @@ -8695,7 +8714,7 @@ impl H2FrameParser { if exclusive_js.is_boolean() { if exclusive_js.as_boolean() { exclusive = true; - stream.exclusive = true; + stream.exclusive.set(true); has_priority = true; } } else { @@ -8712,16 +8731,18 @@ impl H2FrameParser { has_priority = true; parent = parent_js.to_int32(); if parent <= 0 || parent as u32 > MAX_STREAM_ID { - stream.state = StreamState::CLOSED; - stream.rst_code = ErrorCode::INTERNAL_ERROR.0; + stream.state.set(StreamState::CLOSED); + stream.rst_code.set(ErrorCode::INTERNAL_ERROR.0); this.dispatch_with_extra( JSH2FrameParser::Gc::onStreamError, stream.get_identifier(), - JSValue::js_number(stream.rst_code as f64), + JSValue::js_number(stream.rst_code.get() as f64), ); return Ok(JSValue::js_number(stream.id as f64)); } - stream.stream_dependency = u32::try_from(parent).expect("int cast"); + stream + .stream_dependency + .set(u32::try_from(parent).expect("int cast")); } else { return Err(global_object.throw_invalid_argument_type_value( b"options.parent", @@ -8736,16 +8757,16 @@ impl H2FrameParser { has_priority = true; weight = weight_js.to_int32(); if weight < 1 || weight > u8::MAX as i32 { - stream.state = StreamState::CLOSED; - stream.rst_code = ErrorCode::INTERNAL_ERROR.0; + stream.state.set(StreamState::CLOSED); + stream.rst_code.set(ErrorCode::INTERNAL_ERROR.0); this.dispatch_with_extra( JSH2FrameParser::Gc::onStreamError, stream.get_identifier(), - JSValue::js_number(stream.rst_code as f64), + JSValue::js_number(stream.rst_code.get() as f64), ); return Ok(JSValue::js_number(stream_id as f64)); } - stream.weight = u16::try_from(weight).expect("int cast"); + stream.weight.set(u16::try_from(weight).expect("int cast")); } else { return Err(global_object.throw_invalid_argument_type_value( b"options.weight", @@ -8755,17 +8776,17 @@ impl H2FrameParser { } if weight < 1 || weight > u8::MAX as i32 { - stream.state = StreamState::CLOSED; - stream.rst_code = ErrorCode::INTERNAL_ERROR.0; + stream.state.set(StreamState::CLOSED); + stream.rst_code.set(ErrorCode::INTERNAL_ERROR.0); this.dispatch_with_extra( JSH2FrameParser::Gc::onStreamError, stream.get_identifier(), - JSValue::js_number(stream.rst_code as f64), + JSValue::js_number(stream.rst_code.get() as f64), ); return Ok(JSValue::js_number(stream_id as f64)); } - stream.weight = u16::try_from(weight).expect("int cast"); + stream.weight.set(u16::try_from(weight).expect("int cast")); } if let Some(signal_arg) = options.get(global_object, "signal")? { @@ -8773,9 +8794,9 @@ impl H2FrameParser { // SAFETY: `from_js` returns a live *mut AbortSignal owned by JSC; rooted via `signal_arg` on the stack. let signal_ = unsafe { &mut *signal_ptr }; if signal_.aborted() { - stream.state = StreamState::IDLE; + stream.state.set(StreamState::IDLE); let wrapped = Bun__wrapAbortError(global_object, signal_.abort_reason()); - this.abort_stream(&mut stream, wrapped); + this.abort_stream(&stream, wrapped); return Ok(JSValue::js_number(stream_id as f64)); } stream.attach_signal(this, signal_); @@ -8791,13 +8812,13 @@ impl H2FrameParser { // too much memory being use if this.get_session_memory_usage() > this.max_session_memory.get() as usize { - stream.state = StreamState::CLOSED; - stream.rst_code = ErrorCode::ENHANCE_YOUR_CALM.0; + stream.state.set(StreamState::CLOSED); + stream.rst_code.set(ErrorCode::ENHANCE_YOUR_CALM.0); this.rejected_streams.set(this.rejected_streams.get() + 1); this.dispatch_with_extra( JSH2FrameParser::Gc::onStreamError, stream.get_identifier(), - JSValue::js_number(stream.rst_code as f64), + JSValue::js_number(stream.rst_code.get() as f64), ); if this.rejected_streams.get() >= this.max_rejected_streams.get() { let global = this.handlers.get().global(); @@ -8827,8 +8848,8 @@ impl H2FrameParser { if this.max_send_header_block_length.get() != 0 && encoded_size > this.max_send_header_block_length.get() as usize { - stream.state = StreamState::CLOSED; - stream.rst_code = ErrorCode::REFUSED_STREAM.0; + stream.state.set(StreamState::CLOSED); + stream.rst_code.set(ErrorCode::REFUSED_STREAM.0); this.dispatch_with_2_extra( JSH2FrameParser::Gc::onFrameError, @@ -8840,7 +8861,7 @@ impl H2FrameParser { this.dispatch_with_extra( JSH2FrameParser::Gc::onStreamError, stream.get_identifier(), - JSValue::js_number(stream.rst_code as f64), + JSValue::js_number(stream.rst_code.get() as f64), ); return Ok(JSValue::js_number(stream_id as f64)); } @@ -8993,10 +9014,10 @@ impl H2FrameParser { } if end_stream { - stream.end_after_headers = true; + stream.end_after_headers.set(true); if wait_for_trailers { - stream.state = StreamState::HALF_CLOSED_LOCAL; + stream.state.set(StreamState::HALF_CLOSED_LOCAL); this.dispatch(JSH2FrameParser::Gc::onWantTrailers, stream.get_identifier()); return Ok(JSValue::js_number(stream_id as f64)); } @@ -9011,19 +9032,19 @@ impl H2FrameParser { // count) until socket close. let identifier = stream.get_identifier(); identifier.ensure_still_alive(); - if stream.state == StreamState::HALF_CLOSED_REMOTE { - stream.state = StreamState::CLOSED; + if stream.state.get() == StreamState::HALF_CLOSED_REMOTE { + stream.state.set(StreamState::CLOSED); stream.free_resources::(this); } else { - stream.state = StreamState::HALF_CLOSED_LOCAL; + stream.state.set(StreamState::HALF_CLOSED_LOCAL); } this.dispatch_with_extra( JSH2FrameParser::Gc::onStreamEnd, identifier, - JSValue::js_number(stream.state as u8 as f64), + JSValue::js_number(stream.state.get() as u8 as f64), ); } else { - stream.wait_for_trailers = wait_for_trailers; + stream.wait_for_trailers.set(wait_for_trailers); } if silent { diff --git a/src/runtime/api/bun/js_bun_spawn_bindings.rs b/src/runtime/api/bun/js_bun_spawn_bindings.rs index c2034a6a1df5..9949f4d97494 100644 --- a/src/runtime/api/bun/js_bun_spawn_bindings.rs +++ b/src/runtime/api/bun/js_bun_spawn_bindings.rs @@ -1048,12 +1048,10 @@ pub(crate) fn spawn_maybe_sync( // `SpawnSyncEventLoop::init`) so stdio readers/writers register on it // instead of the main loop. let event_loop: *mut jsc::event_loop::EventLoop = if IS_SYNC { - // SAFETY: see note above; `spawn_sync_event_loop` re-borrows the - // same VM via the raw pointer for its `vm` arg. + // SAFETY: see note above; `spawn_sync_event_loop` stores the VM pointer + // type-erased, so it takes the raw pointer and creates no second `&mut`. unsafe { - let sync_loop = (*jsc_vm_ptr) - .rare_data() - .spawn_sync_event_loop(&mut *jsc_vm_ptr); + let sync_loop = (*jsc_vm_ptr).rare_data().spawn_sync_event_loop(jsc_vm_ptr); sync_loop.prepare(jsc_vm_ptr.cast()); // `SpawnSyncEventLoop.event_loop` is type-erased to `*mut ()` // (bun_event_loop is below bun_jsc); the accessor returns the @@ -1079,7 +1077,7 @@ pub(crate) fn spawn_maybe_sync( let main_loop = (*jsc_vm_ptr_cleanup).event_loop(); (*jsc_vm_ptr_cleanup) .rare_data() - .spawn_sync_event_loop(&mut *jsc_vm_ptr_cleanup) + .spawn_sync_event_loop(jsc_vm_ptr_cleanup) .cleanup(jsc_vm_ptr_cleanup.cast(), main_loop.cast()); } } @@ -1258,9 +1256,10 @@ pub(crate) fn spawn_maybe_sync( // address-dependent fields (maxbufs, ipc_data on Windows) afterward. let subprocess_ptr = bun_core::heap::into_raw(Box::new(SubprocessT { global_this: bun_ptr::BackRef::new(global_this), - // SAFETY: `to_process` returns a non-null `Box::into_raw` pointer; the - // intrusive ref is released in `Subprocess::finalize`. - process: unsafe { bun_ptr::BackRef::from_raw(process) }, + // SAFETY: `to_process` returns a non-null `Box::into_raw` pointer with + // exactly one ref; it transfers here and is released in + // `Subprocess::finalize`. + process: unsafe { bun_ptr::RefPtr::adopt_ref(process) }, pid_rusage: Cell::new(None), // stdin/stdout/stderr are assigned immediately after this literal. // `Writable.init()` writes to `subprocess.weak_file_sink_stdin_ptr`, @@ -1305,8 +1304,10 @@ pub(crate) fn spawn_maybe_sync( )), exited_due_to_maxbuf: Cell::new(None), })); - // SAFETY: subprocess_ptr is a freshly-boxed Subprocess; we hold the only reference. - let subprocess = unsafe { &mut *subprocess_ptr }; + // SAFETY: subprocess_ptr is a freshly-boxed Subprocess. A shared ref suffices: + // every field is `Cell`/`JsCell`, so the writes this fn makes through + // `subprocess_ptr` (abort listener, JS re-entry) cannot invalidate it. + let subprocess = unsafe { &*subprocess_ptr }; // Erase the borrow lifetime to 'static for the intrusive back-pointer // (PipeReader stores it as raw NonNull). subprocess_ptr is non-null (just boxed). let subprocess_nn: NonNull> = @@ -1381,10 +1382,9 @@ pub(crate) fn spawn_maybe_sync( } subprocess.finalize_streams(); subprocess.process_mut().detach(); - // Release the intrusive ref - // (finalize() won't run on this error path). - // SAFETY: this error path returns without ever reading `process` again. - unsafe { Process::deref(subprocess.process.as_ptr()) }; + // Release the intrusive ref (finalize() won't run on this error + // path); nothing reads `process` afterwards. + subprocess.process.deref(); let mut mb = subprocess.stdout_maxbuf.get(); MaxBuf::remove_from_subprocess(&mut mb); subprocess.stdout_maxbuf.set(mb); @@ -1472,11 +1472,11 @@ pub(crate) fn spawn_maybe_sync( #[cfg(unix)] if !IS_SYNC { if let Some(mode) = maybe_ipc_mode { - // SAFETY: re-borrow `jsc_vm` through the raw pointer for the nested - // `vm` arg while `rare_data()` holds the outer &mut. + // SAFETY: `jsc_vm_ptr` is the live thread VM; `spawn_ipc_group` takes a + // shared `&VirtualMachine`, so the nested arg needs no second `&mut`. let raw_socket = unsafe { &mut *jsc_vm_ptr } .rare_data() - .spawn_ipc_group(unsafe { &mut *jsc_vm_ptr }) + .spawn_ipc_group(unsafe { &*jsc_vm_ptr }) .from_fd( bun_uws::SocketKind::SpawnIpc, None, @@ -1766,11 +1766,8 @@ pub(crate) fn spawn_maybe_sync( if !IS_SYNC { if !subprocess.has_exited() { // SAFETY: jsc_vm_ptr points to the live thread VM; `subprocess.process` - // is a `BackRef` (wraps `NonNull`), so its pointer is non-null. - unsafe { - (*jsc_vm_ptr) - .on_subprocess_spawn(NonNull::new_unchecked(subprocess.process.as_ptr())) - }; + // is a `RefPtr` (wraps `NonNull`), so its pointer is non-null. + unsafe { (*jsc_vm_ptr).on_subprocess_spawn(subprocess.process.data) }; } return Ok(out); } @@ -1780,8 +1777,7 @@ pub(crate) fn spawn_maybe_sync( debug_assert!(IS_SYNC); if can_block_entire_thread_to_reduce_cpu_usage_in_fast_path { - // SAFETY: jsc_vm_ptr is the live thread VM. - unsafe { &mut *jsc_vm_ptr } + jsc_vm .counters .mark(jsc::counters::Field::SpawnSyncBlocking); let debug_timer = Output::DebugTimer::start(); @@ -1813,10 +1809,8 @@ pub(crate) fn spawn_maybe_sync( if !subprocess.has_exited() { // SAFETY: jsc_vm_ptr points to the live thread VM; `subprocess.process` - // is a `BackRef` (wraps `NonNull`), so its pointer is non-null. - unsafe { - (*jsc_vm_ptr).on_subprocess_spawn(NonNull::new_unchecked(subprocess.process.as_ptr())) - }; + // is a `RefPtr` (wraps `NonNull`), so its pointer is non-null. + unsafe { (*jsc_vm_ptr).on_subprocess_spawn(subprocess.process.data) }; } let mut did_timeout = false; @@ -1859,10 +1853,11 @@ pub(crate) fn spawn_maybe_sync( let has_user_timespec = !user_timespec.eql(&Timespec::EPOCH); let mut bun_test_fired = false; - // SAFETY: jsc_vm_ptr is the live thread VM; re-borrowed for the nested arg. + // SAFETY: jsc_vm_ptr is the live thread VM; the `vm` arg is the raw + // pointer, so no `&mut VirtualMachine` spans the tick loop below. let sync_loop = unsafe { &mut *jsc_vm_ptr } .rare_data() - .spawn_sync_event_loop(unsafe { &mut *jsc_vm_ptr }); + .spawn_sync_event_loop(jsc_vm_ptr); while subprocess.compute_has_pending_activity() { // Re-evaluate this at each iteration of the loop since it may change between iterations. @@ -1934,10 +1929,9 @@ pub(crate) fn spawn_maybe_sync( let taken_active_file = active_file_strong.take().unwrap(); - // SAFETY: jsc_vm_ptr is the live thread VM. crate::test_runner::jest::Jest::runner() .unwrap() - .remove_active_timeout(unsafe { &mut *jsc_vm_ptr }); + .remove_active_timeout(jsc_vm); // This might internally call `kill(2)` on this // spawnSync process. Even if we do that, we still @@ -1992,7 +1986,7 @@ pub(crate) fn spawn_maybe_sync( let result_pid = JSValue::js_number_from_int32(subprocess.pid()); // SAFETY: `subprocess_ptr` was produced by `heap::into_raw(Box::new(...))` // above (spawnSync path: never handed to a JS wrapper); reclaim ownership. - // `subprocess` (`&mut *subprocess_ptr`) is not used after this line. + // `subprocess` (`&*subprocess_ptr`) is not used after this line. SubprocessT::finalize(unsafe { Box::from_raw(subprocess_ptr) }); let sync_value = JSValue::create_empty_object(global_this, 0); diff --git a/src/runtime/api/bun/subprocess.rs b/src/runtime/api/bun/subprocess.rs index fd2189a512cf..23056f663820 100644 --- a/src/runtime/api/bun/subprocess.rs +++ b/src/runtime/api/bun/subprocess.rs @@ -122,13 +122,13 @@ pub use bun_spawn::process::StdioKind; pub struct Subprocess<'a> { pub ref_count: RefCount>, /// Intrusively-refcounted `Process`. Allocated via - /// `heap::alloc` in `Process::init_posix`/`init_windows`; the +1 ref - /// from construction is released in [`Subprocess::finalize`] via - /// `Process::deref()`. Not `Arc` — `Process` carries its own - /// `ThreadSafeRefCount` and crosses the `ProcessAutoKiller`/waiter-thread - /// boundary by raw identity, so wrapping in `Arc` would double-count and - /// (worse) `Arc::from_raw` on a `Box` allocation is UB. - pub process: bun_ptr::BackRef, + /// `heap::alloc` in `Process::init_posix`/`init_windows`; this handle owns + /// the +1 ref from construction, released in [`Subprocess::finalize`] (or + /// the `spawn_maybe_sync` error path) via `RefPtr::deref()`. Not `Arc` — + /// `Process` carries its own `ThreadSafeRefCount` and crosses the + /// `ProcessAutoKiller`/waiter-thread boundary by raw identity, so wrapping + /// in `Arc` would double-count and `Arc::from_raw` on a `Box` alloc is UB. + pub process: RefPtr, pub stdin: JsCell>, pub stdout: JsCell, pub stderr: JsCell, @@ -228,22 +228,20 @@ impl<'a> Subprocess<'a> { /// the raw pointer is sound. #[inline] pub fn process(&self) -> &Process { - self.process.get() + self.process.data() } /// Mutably borrow the owned [`Process`]. /// - /// Centralises the `BackRef → &mut Process` projection so callers + /// Centralises the `RefPtr → &mut Process` projection so callers /// (including `js_bun_spawn_bindings`) stay safe. Caller must be on the /// owning JS thread with no other live `&mut Process`. #[inline] #[allow(clippy::mut_from_ref)] pub(super) fn process_mut(&self) -> &mut Process { - // SAFETY: see `process()` — all access is on the single JS-mutator - // thread. R-2: `&self` - // (interior-mutability) so callers don't need `&mut Subprocess`; - // `Process` lives in a separate allocation (BackRef) so the returned - // `&mut` never aliases `*self`. Single JS-mutator thread. + // SAFETY: `RefPtr::as_ptr` is the sanctioned mutation route and we own + // a ref, so the pointee is live. `Process` is a separate allocation, so + // the `&mut` never aliases `*self`. Single JS-mutator thread. unsafe { &mut *self.process.as_ptr() } } @@ -1264,12 +1262,10 @@ impl Subprocess<'_> { if exit_handler_pending { this.deref(); } - // Release the intrusive ref now, - // not when `ref_count` → 0. The raw `*mut Process` is left dangling but - // no code path reads `this.process` after this (finalize runs once). - // SAFETY: `process` is the live Box-backed Process; deref() frees it - // when its own ThreadSafeRefCount reaches zero. - unsafe { Process::deref(this.process.as_ptr()) }; + // Release the intrusive ref now, not when `ref_count` → 0. The handle + // is left dangling but no code path reads `this.process` after this + // (finalize runs once); `deref()` frees the Box at zero. + this.process.deref(); if this.event_loop_timer.get().state == EventLoopTimerState::ACTIVE { Self::timer_all().remove(this.event_loop_timer.as_ptr()); diff --git a/src/runtime/api/bun/subprocess/Writable.rs b/src/runtime/api/bun/subprocess/Writable.rs index 01125952ff25..73736f99a301 100644 --- a/src/runtime/api/bun/subprocess/Writable.rs +++ b/src/runtime/api/bun/subprocess/Writable.rs @@ -43,20 +43,6 @@ impl<'a> Writable<'a> { bun_ptr::BackRef::from(pipe) } - /// Mutable counterpart to [`pipe_sink`](Self::pipe_sink). - /// - /// Same invariant: `Writable::Pipe` holds a +1 intrusive ref on the - /// `FileSink` for the variant's lifetime, and the sink lives in its own - /// allocation (disjoint from both the `Writable` value and the parent - /// `Subprocess`), so projecting `&mut` here cannot alias any other live - /// borrow. Single JS-mutator thread — no concurrent `&mut FileSink`. - #[inline] - #[allow(clippy::mut_from_ref)] - pub(in crate::api) fn pipe_sink_mut(pipe: &NonNull) -> &mut FileSink { - // SAFETY: see fn doc — +1-intrusive-ref'd, heap-disjoint, single-thread. - unsafe { &mut *pipe.as_ptr() } - } - /// Release one intrusive ref on a `FileSink` held by `Writable::Pipe` /// (or freshly returned from `FileSink::create*`). Centralises the /// `unsafe { FileSink::deref(ptr) }` so callers stay safe — same @@ -175,7 +161,7 @@ impl<'a> Writable<'a> { pub fn init( stdio: &mut Stdio, event_loop: &EventLoop, - subprocess: &mut Subprocess<'a>, + subprocess: &Subprocess<'a>, result: StdioResult, promise_for_stream: &mut JSValue, ) -> Result, bun_core::Error> { @@ -207,7 +193,7 @@ impl<'a> Writable<'a> { let pipe_nn = NonNull::new(FileSink::create_with_pipe(evtloop, uv_pipe)) .expect("FileSink::create_with_pipe returns non-null"); let pipe_ptr = pipe_nn.as_ptr(); - let pipe = Self::pipe_sink_mut(&pipe_nn); + let pipe = Self::pipe_sink(pipe_nn); match pipe.writer.with_mut(|w| w.start_with_current_pipe()) { bun_sys::Result::Ok(()) => {} @@ -228,7 +214,10 @@ impl<'a> Writable<'a> { }); if let Stdio::ReadableStream(rs) = stdio { - let assign_result = pipe.assign_to_stream(rs, global); + // SAFETY: canonical `*mut FileSink` (+1 held by the + // enum); no `&FileSink` spans the JS re-entry. + let assign_result = + unsafe { FileSink::assign_to_stream(pipe_ptr, rs, global) }; if let Some(err_val) = assign_result.to_error() { subprocess.weak_file_sink_stdin_ptr.set(None); subprocess.update_flags(|f| { @@ -259,7 +248,7 @@ impl<'a> Writable<'a> { }; return Ok(Writable::Buffer(StaticPipeWriter::create( evtloop, - subprocess as *mut Subprocess<'a>, + std::ptr::from_ref::>(subprocess).cast_mut(), result, super::source_from_blob(blob), ))); @@ -267,7 +256,7 @@ impl<'a> Writable<'a> { Stdio::ArrayBuffer(array_buffer) => { return Ok(Writable::Buffer(StaticPipeWriter::create( evtloop, - subprocess as *mut Subprocess<'a>, + std::ptr::from_ref::>(subprocess).cast_mut(), result, super::source_from_array_buffer(core::mem::take(array_buffer)), ))); @@ -306,7 +295,7 @@ impl<'a> Writable<'a> { // `create` returns a freshly-boxed non-null pointer. let pipe_nn = NonNull::new(FileSink::create(evtloop, result.unwrap())) .expect("FileSink::create returns non-null"); - let pipe = Self::pipe_sink_mut(&pipe_nn); + let pipe = Self::pipe_sink(pipe_nn); match pipe.writer.with_mut(|w| w.start(pipe.fd.get(), true)) { bun_sys::Result::Ok(()) => {} @@ -336,7 +325,10 @@ impl<'a> Writable<'a> { }); if let Stdio::ReadableStream(rs) = stdio { - let assign_result = pipe.assign_to_stream(rs, global); + // SAFETY: canonical `*mut FileSink` (+1 held by the enum); + // no `&FileSink` spans the JS re-entry. + let assign_result = + unsafe { FileSink::assign_to_stream(pipe_nn.as_ptr(), rs, global) }; if let Some(err_val) = assign_result.to_error() { subprocess.weak_file_sink_stdin_ptr.set(None); subprocess.update_flags(|f| f.set(Flags::DEREF_ON_STDIN_DESTROYED, false)); @@ -364,14 +356,14 @@ impl<'a> Writable<'a> { }; Ok(Writable::Buffer(StaticPipeWriter::create( evtloop, - std::ptr::from_mut::>(subprocess), + std::ptr::from_ref::>(subprocess).cast_mut(), result, super::source_from_blob(blob), ))) } Stdio::ArrayBuffer(array_buffer) => Ok(Writable::Buffer(StaticPipeWriter::create( evtloop, - std::ptr::from_mut::>(subprocess), + std::ptr::from_ref::>(subprocess).cast_mut(), result, super::source_from_array_buffer(core::mem::take(array_buffer)), ))), @@ -452,11 +444,12 @@ impl<'a> Writable<'a> { // enum's create-time +1 now that the wrapper holds its own // — mirrors Blob.rs:1899-1902. `stdin` was already swapped // to `Ignore` above so `on_close` won't double-release. - let js = Self::pipe_sink_mut(&pipe_nn).to_js(global_this); + // SAFETY: canonical `*mut FileSink`, +1 held by the enum. + let js = unsafe { FileSink::to_js(pipe_nn.as_ptr(), global_this) }; Self::pipe_release(pipe_nn); js } else { - let pipe = Self::pipe_sink_mut(&pipe_nn); + let pipe = Self::pipe_sink(pipe_nn); subprocess.update_flags(|f| f.set(Flags::HAS_STDIN_DESTRUCTOR_CALLED, false)); subprocess.weak_file_sink_stdin_ptr.set(Some(pipe_nn)); if !subprocess @@ -478,12 +471,17 @@ impl<'a> Writable<'a> { // Rust `FileSink::to_js_with_destructor` takes its own // per-wrapper +1; release the enum's create-time +1 (see // the has-exited arm above and Blob.rs:1899-1902). - let js = pipe.to_js_with_destructor( - global_this, - Some(sink::destructor_ptr_subprocess( - subprocess.as_ctx_ptr().cast::(), - )), - ); + // SAFETY: canonical `*mut FileSink`, +1 held by the enum; no + // `&FileSink` spans the wrapper creation. + let js = unsafe { + FileSink::to_js_with_destructor( + pipe_nn.as_ptr(), + global_this, + Some(sink::destructor_ptr_subprocess( + subprocess.as_ctx_ptr().cast::(), + )), + ) + }; Self::pipe_release(pipe_nn); js } @@ -505,7 +503,7 @@ impl<'a> Writable<'a> { let parent_ptr = NonNull::new(subprocess.as_ctx_ptr().cast::()); match subprocess.stdin.replace(Writable::Ignore) { Writable::Pipe(pipe_nn) => { - let pipe = Self::pipe_sink_mut(&pipe_nn); + let pipe = Self::pipe_sink(pipe_nn); if pipe.signal.get().ptr == parent_ptr { pipe.signal.with_mut(|s| s.clear()); } diff --git a/src/runtime/api/cron.rs b/src/runtime/api/cron.rs index a74a28d51fb7..e0f0ff7bfdf1 100644 --- a/src/runtime/api/cron.rs +++ b/src/runtime/api/cron.rs @@ -65,17 +65,14 @@ use crate::jsc_hooks::timer_all_mut as timer_all; // ============================================================================ /// Shared base for [`CronRegisterJob`] and [`CronRemoveJob`]. -// Note: every method on the path to `finish()` (which `heap::take`- -// drops `this`) takes a raw `*mut Self` receiver. -// A `&mut self` *parameter* would carry a Stacked Borrows FnEntry protector, -// making the in-flight dealloc UB; a *local* `let s = &mut *this` reborrow -// has no protector and ends at last use under NLL, so field access via `s` -// followed by `Self::finish(this)` is sound. +// Note: every method on the path to `finish()` (which `heap::take`-drops +// `this`) takes a raw `*mut Self` receiver and reborrows it *shared*; all +// mutable state lives behind per-field `Cell` (Copy) / `JsCell` (non-Copy). trait CronJobBase: Sized { - fn remaining_fds_mut(&mut self) -> &mut i8; - fn err_msg_mut(&mut self) -> &mut Option>; - fn has_called_process_exit_mut(&mut self) -> &mut bool; - fn exit_status_mut(&mut self) -> &mut Option; + fn remaining_fds_mut(&self) -> &Cell; + fn err_msg_mut(&self) -> &JsCell>>; + fn has_called_process_exit_mut(&self) -> &Cell; + fn exit_status_mut(&self) -> &JsCell>; /// May free `this`. Caller must not touch `this` afterward. unsafe fn maybe_finished(this: *mut Self); @@ -88,40 +85,40 @@ trait CronJobBase: Sized { /// May free `this` via `maybe_finished`. unsafe fn on_reader_done(this: *mut Self) { - // SAFETY: local reborrow, no protector; ends before `maybe_finished`. - let s = unsafe { &mut *this }; - debug_assert!(*s.remaining_fds_mut() > 0); - *s.remaining_fds_mut() -= 1; - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + // SAFETY: shared reborrow of the live heap job; ends before `maybe_finished`. + let s = unsafe { &*this }; + debug_assert!(s.remaining_fds_mut().get() > 0); + s.remaining_fds_mut().set(s.remaining_fds_mut().get() - 1); + // SAFETY: `this` is the live heap job. unsafe { Self::maybe_finished(this) }; } /// May free `this` via `maybe_finished`. unsafe fn on_reader_error(this: *mut Self, err: sys::Error) { - // SAFETY: local reborrow, no protector; ends before `maybe_finished`. - let s = unsafe { &mut *this }; - debug_assert!(*s.remaining_fds_mut() > 0); - *s.remaining_fds_mut() -= 1; - if s.err_msg_mut().is_none() { + // SAFETY: shared reborrow of the live heap job; ends before `maybe_finished`. + let s = unsafe { &*this }; + debug_assert!(s.remaining_fds_mut().get() > 0); + s.remaining_fds_mut().set(s.remaining_fds_mut().get() - 1); + if s.err_msg_mut().get().is_none() { let mut msg = Vec::new(); let _ = write!( &mut msg, "Failed to read process output: {}", <&'static str>::from(err.get_errno()) ); - *s.err_msg_mut() = Some(msg); + s.err_msg_mut().set(Some(msg)); } - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + // SAFETY: `this` is the live heap job. unsafe { Self::maybe_finished(this) }; } /// May free `this` via `maybe_finished`. unsafe fn on_process_exit(this: *mut Self, _proc: &Process, status: Status, _rusage: &Rusage) { - // SAFETY: local reborrow, no protector; ends before `maybe_finished`. - let s = unsafe { &mut *this }; - *s.has_called_process_exit_mut() = true; - *s.exit_status_mut() = Some(status); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + // SAFETY: shared reborrow of the live heap job; ends before `maybe_finished`. + let s = unsafe { &*this }; + s.has_called_process_exit_mut().set(true); + s.exit_status_mut().set(Some(status)); + // SAFETY: `this` is the live heap job. unsafe { Self::maybe_finished(this) }; } } @@ -144,17 +141,17 @@ pub struct CronRegisterJob { #[cfg(windows)] parsed_cron: CronExpression, - state: RegisterState, + state: Cell, // LIFETIMES.tsv: SHARED — `Process` is intrusively refcounted (`*mut`). - process: Option<*mut Process>, - stdout_reader: OutputReader, + process: Cell>, + stdout_reader: JsCell, #[cfg(windows)] - stderr_reader: OutputReader, - remaining_fds: i8, - has_called_process_exit: bool, - exit_status: Option, - err_msg: Option>, - tmp_path: Option, + stderr_reader: JsCell, + remaining_fds: Cell, + has_called_process_exit: Cell, + exit_status: JsCell>, + err_msg: JsCell>>, + tmp_path: JsCell>, /// Typed enum for the io-layer FilePoll vtable (`bun_io::EventLoopHandle` /// wraps `*const EventLoopHandle`). event_loop_handle: EventLoopHandle, @@ -186,17 +183,17 @@ bun_io::impl_buffered_reader_parent! { } impl CronJobBase for CronRegisterJob { - fn remaining_fds_mut(&mut self) -> &mut i8 { - &mut self.remaining_fds + fn remaining_fds_mut(&self) -> &Cell { + &self.remaining_fds } - fn err_msg_mut(&mut self) -> &mut Option> { - &mut self.err_msg + fn err_msg_mut(&self) -> &JsCell>> { + &self.err_msg } - fn has_called_process_exit_mut(&mut self) -> &mut bool { - &mut self.has_called_process_exit + fn has_called_process_exit_mut(&self) -> &Cell { + &self.has_called_process_exit } - fn exit_status_mut(&mut self) -> &mut Option { - &mut self.exit_status + fn exit_status_mut(&self) -> &JsCell> { + &self.exit_status } unsafe fn maybe_finished(this: *mut Self) { // SAFETY: caller guarantees `this` is the live heap job with no active borrows. @@ -205,20 +202,20 @@ impl CronJobBase for CronRegisterJob { } impl CronRegisterJob { - fn set_err(&mut self, args: core::fmt::Arguments<'_>) { - if self.err_msg.is_none() { + fn set_err(&self, args: core::fmt::Arguments<'_>) { + if self.err_msg.get().is_none() { let mut msg = Vec::new(); let _ = msg.write_fmt(args); - self.err_msg = Some(msg); + self.err_msg.set(Some(msg)); } } /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. unsafe fn maybe_finished(this: *mut Self) { - // SAFETY: local reborrow (no FnEntry protector); not used after any - // call below that may free `this`. - let s = unsafe { &mut *this }; - if !s.has_called_process_exit || s.remaining_fds != 0 { + // SAFETY: shared reborrow of the live heap job; every write goes + // through an interior-mutable field, so no `&mut Self` is ever live. + let s = unsafe { &*this }; + if !s.has_called_process_exit.get() || s.remaining_fds.get() != 0 { return; } if let Some(proc) = s.process.take() { @@ -228,30 +225,27 @@ impl CronRegisterJob { Process::deref(proc); } } - if s.err_msg.is_some() { - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + if s.err_msg.get().is_some() { + // SAFETY: `this` is the live heap job. + return Self::finish(unsafe { bun_core::heap::take(this) }); } - let Some(status) = s.exit_status.take() else { + let Some(status) = s.exit_status.replace(None) else { return; }; match status { Status::Exited(exited) => { if exited.code != 0 - && !(s.state == RegisterState::ReadingCrontab && exited.code == 1) - && s.state != RegisterState::BootingOut + && !(s.state.get() == RegisterState::ReadingCrontab && exited.code == 1) + && s.state.get() != RegisterState::BootingOut { - // Materialize the trimmed stderr into an owned buffer: - // `final_buffer()` borrows `s` mutably, and `set_err` - // below needs another `&mut s` — copy out so the two - // borrows do not overlap (Windows only; POSIX ignores - // stderr here). + // Copy the trimmed stderr out: the `&mut OutputReader` must + // not survive the `set_err` calls below (Windows only; + // POSIX ignores stderr here). The closure reaches no JS. #[cfg(windows)] - let stderr_owned: Vec = bun_core::strings::trim( - s.stderr_reader.final_buffer().as_slice(), - &ASCII_WHITESPACE, - ) - .to_vec(); + let stderr_owned: Vec = s.stderr_reader.with_mut(|r| { + bun_core::strings::trim(r.final_buffer().as_slice(), &ASCII_WHITESPACE) + .to_vec() + }); #[cfg(windows)] let stderr_output: &[u8] = stderr_owned.as_slice(); #[cfg(not(windows))] @@ -260,7 +254,7 @@ impl CronRegisterJob { // a clear message instead of the raw schtasks output. #[cfg(windows)] { - if s.state == RegisterState::InstallingCrontab + if s.state.get() == RegisterState::InstallingCrontab && bun_core::index_of( stderr_output, b"No mapping between account names", @@ -273,7 +267,7 @@ impl CronRegisterJob { To fix this, either run Bun as a regular user account, or create the scheduled task manually with: \ schtasks /create /xml /tn /ru SYSTEM /f" )); - return unsafe { Self::finish(this) }; + return Self::finish(unsafe { bun_core::heap::take(this) }); } } if !stderr_output.is_empty() { @@ -281,15 +275,15 @@ impl CronRegisterJob { } else { s.set_err(format_args!("Process exited with code {}", exited.code)); } - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + // SAFETY: `this` is the live heap job. + return Self::finish(unsafe { bun_core::heap::take(this) }); } } Status::Signaled(sig) => { - if s.state != RegisterState::BootingOut { + if s.state.get() != RegisterState::BootingOut { s.set_err(format_args!("Process killed by signal {}", sig as i32)); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + // SAFETY: `this` is the live heap job. + return Self::finish(unsafe { bun_core::heap::take(this) }); } } Status::Err(err) => { @@ -297,80 +291,78 @@ impl CronRegisterJob { "Process error: {}", <&'static str>::from(err.get_errno()) )); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + // SAFETY: `this` is the live heap job. + return Self::finish(unsafe { bun_core::heap::take(this) }); } Status::Running => return, } - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + // SAFETY: `this` is the live heap job. unsafe { Self::advance_state(this) }; } /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. unsafe fn advance_state(this: *mut Self) { - // SAFETY: local reborrow; last use precedes any self-freeing call. - let s = unsafe { &mut *this }; + // SAFETY: shared reborrow; writes go through the interior-mutable fields. + let s = unsafe { &*this }; #[cfg(target_os = "macos")] { - match s.state { - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + match s.state.get() { + // SAFETY: `this` is the live heap job. RegisterState::WritingPlist => unsafe { Self::spawn_bootout(this) }, // SAFETY: local reborrow `s` has ended; `this` is the live heap job. RegisterState::BootingOut => unsafe { Self::spawn_bootstrap(this) }, // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - RegisterState::Bootstrapping => unsafe { Self::finish(this) }, + RegisterState::Bootstrapping => Self::finish(unsafe { bun_core::heap::take(this) }), _ => { s.set_err(format_args!("Unexpected state")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::finish(this) }; + Self::finish(unsafe { bun_core::heap::take(this) }); } } } #[cfg(not(target_os = "macos"))] { - match s.state { - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + match s.state.get() { + // SAFETY: `this` is the live heap job. RegisterState::ReadingCrontab => unsafe { Self::process_crontab_and_install(this) }, - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - RegisterState::InstallingCrontab => unsafe { Self::finish(this) }, + RegisterState::InstallingCrontab => { + // SAFETY: the local reborrow has ended; `this` is the unique live heap job. + Self::finish(unsafe { bun_core::heap::take(this) }) + } _ => { s.set_err(format_args!("Unexpected state")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::finish(this) }; + Self::finish(unsafe { bun_core::heap::take(this) }); } } } } - /// Consumes and frees `this` (`heap::take`). - unsafe fn finish(this: *mut Self) { - // SAFETY: caller holds the unique Box; consumed below. Local - // reborrow has no FnEntry protector and is not used after the drop. - let this_ref = unsafe { &mut *this }; - this_ref.state = if this_ref.err_msg.is_some() { + /// Consumes and frees the job, settling its promise. + fn finish(mut self: Box) { + self.state.set(if self.err_msg.get().is_some() { RegisterState::Failed } else { RegisterState::Done - }; - this_ref.poll.unref(bun_io::js_vm_ctx()); + }); + self.poll.unref(bun_io::js_vm_ctx()); let ev = VirtualMachine::get().event_loop_mut(); ev.enter(); - if let Some(msg) = &this_ref.err_msg { - let _ = this_ref.promise.reject_with_async_stack( - &this_ref.global, - Ok(this_ref + // Move the message out first: settling the promise re-enters JS. + let err_msg = self.err_msg.replace(None); + if let Some(msg) = &err_msg { + let _ = self.promise.reject_with_async_stack( + &self.global, + Ok(self .global .create_error_instance(format_args!("{}", bstr::BStr::new(msg)))), ); } else { - let _ = this_ref - .promise - .resolve(&this_ref.global, JSValue::UNDEFINED); + let _ = self.promise.resolve(&self.global, JSValue::UNDEFINED); } // Drop runs INSIDE the enter/exit scope so Process detach/deref and // reader teardown observe the entered event-loop state. - // SAFETY: `this` was created via heap::alloc in cron_register. - unsafe { drop(bun_core::heap::take(this)) }; + drop(self); ev.exit(); } @@ -390,15 +382,16 @@ impl CronRegisterJob { /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. #[cfg(all(not(target_os = "macos"), not(windows)))] unsafe fn start_linux(this: *mut Self) { - // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. - let s = unsafe { &mut *this }; - s.state = RegisterState::ReadingCrontab; - s.stdout_reader = OutputReader::init::(); - s.stdout_reader.set_parent(this.cast()); + // SAFETY: shared reborrow; writes go through the interior-mutable fields. + let s = unsafe { &*this }; + s.state.set(RegisterState::ReadingCrontab); + s.stdout_reader.set(OutputReader::init::()); + // `set_parent` only stores a pointer — the closure reaches no JS. + s.stdout_reader.with_mut(|r| r.set_parent(this.cast())); let Some(crontab_path) = find_crontab() else { s.set_err(format_args!("crontab not found in PATH")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + // SAFETY: `this` is the live heap job. + return Self::finish(unsafe { bun_core::heap::take(this) }); }; let mut argv: [*const c_char; 3] = [crontab_path, c"-l".as_ptr(), core::ptr::null()]; // SAFETY: local reborrow `s` has ended; `this` is the live heap job. @@ -408,15 +401,18 @@ impl CronRegisterJob { /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. #[cfg(not(target_os = "macos"))] unsafe fn process_crontab_and_install(this: *mut Self) { - // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. - let s = unsafe { &mut *this }; - let existing_content = s.stdout_reader.final_buffer().as_slice(); + // SAFETY: shared reborrow; writes go through the interior-mutable fields. + let s = unsafe { &*this }; let mut result: Vec = Vec::new(); - if filter_crontab(existing_content, s.title.as_bytes(), &mut result).is_err() { + // `final_buffer()` needs `&mut OutputReader`; the closure reaches no JS. + let filtered = s.stdout_reader.with_mut(|r| { + filter_crontab(r.final_buffer().as_slice(), s.title.as_bytes(), &mut result) + }); + if filtered.is_err() { s.set_err(format_args!("Out of memory building crontab")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + // SAFETY: `this` is the live heap job. + return Self::finish(unsafe { bun_core::heap::take(this) }); } // Build new entry with single-quoted paths to prevent shell injection @@ -433,7 +429,7 @@ impl CronRegisterJob { { s.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + return Self::finish(unsafe { bun_core::heap::take(this) }); } result.extend_from_slice(&new_entry); @@ -442,40 +438,40 @@ impl CronRegisterJob { Err(_) => { s.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + return Self::finish(unsafe { bun_core::heap::take(this) }); } }; let tmp_path_ptr = tmp_path.as_ptr(); - s.tmp_path = Some(tmp_path); + s.tmp_path.set(Some(tmp_path)); let file = match File::openat( Fd::cwd(), - s.tmp_path.as_ref().unwrap(), + s.tmp_path.get().as_ref().unwrap(), sys::O::WRONLY | sys::O::CREAT | sys::O::EXCL, 0o600, ) { Ok(f) => f, Err(_) => { s.set_err(format_args!("Failed to create temp file")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + // SAFETY: `this` is the live heap job. + return Self::finish(unsafe { bun_core::heap::take(this) }); } }; if file.write_all(&result).is_err() { let _ = file.close(); // close error is non-actionable s.set_err(format_args!("Failed to write temp file")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + // SAFETY: `this` is the live heap job. + return Self::finish(unsafe { bun_core::heap::take(this) }); } let _ = file.close(); // close error is non-actionable - s.state = RegisterState::InstallingCrontab; + s.state.set(RegisterState::InstallingCrontab); // Note: explicit deinit of old reader before reassign — Drop handles it. - s.stdout_reader = OutputReader::init::(); + s.stdout_reader.set(OutputReader::init::()); let Some(crontab_path) = find_crontab() else { s.set_err(format_args!("crontab not found in PATH")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + return Self::finish(unsafe { bun_core::heap::take(this) }); }; let mut argv: [*const c_char; 3] = [crontab_path, tmp_path_ptr.cast(), core::ptr::null()]; // SAFETY: local reborrow `s` has ended; `this` is the live heap job. @@ -487,23 +483,23 @@ impl CronRegisterJob { /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. #[cfg(target_os = "macos")] unsafe fn start_mac(this: *mut Self) { - // SAFETY: local reborrow; not used after `spawn_bootout`/`finish`. - let s = unsafe { &mut *this }; - s.state = RegisterState::WritingPlist; + // SAFETY: shared reborrow; writes go through the interior-mutable fields. + let s = unsafe { &*this }; + s.state.set(RegisterState::WritingPlist); let calendar_xml = match cron_to_calendar_interval(s.schedule.as_bytes()) { Ok(x) => x, Err(_) => { s.set_err(format_args!("Invalid cron expression")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + return Self::finish(unsafe { bun_core::heap::take(this) }); } }; let Some(home) = env_var::HOME.get() else { s.set_err(format_args!("HOME environment variable not set")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + return Self::finish(unsafe { bun_core::heap::take(this) }); }; let mut launch_agents_dir = Vec::new(); @@ -517,7 +513,7 @@ impl CronRegisterJob { "Failed to create ~/Library/LaunchAgents directory" )); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + return Self::finish(unsafe { bun_core::heap::take(this) }); } let plist_path = match alloc_print_z(format_args!( @@ -529,10 +525,10 @@ impl CronRegisterJob { Err(_) => { s.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + return Self::finish(unsafe { bun_core::heap::take(this) }); } }; - s.tmp_path = Some(plist_path); + s.tmp_path.set(Some(plist_path)); // XML-escape all dynamic values macro_rules! try_escape { @@ -542,7 +538,7 @@ impl CronRegisterJob { Err(_) => { s.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + return Self::finish(unsafe { bun_core::heap::take(this) }); } } }; @@ -587,12 +583,12 @@ impl CronRegisterJob { { s.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + return Self::finish(unsafe { bun_core::heap::take(this) }); } let file = match File::openat( Fd::cwd(), - s.tmp_path.as_ref().unwrap(), + s.tmp_path.get().as_ref().unwrap(), sys::O::WRONLY | sys::O::CREAT | sys::O::TRUNC, 0o644, ) { @@ -600,14 +596,14 @@ impl CronRegisterJob { Err(_) => { s.set_err(format_args!("Failed to create plist file")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + return Self::finish(unsafe { bun_core::heap::take(this) }); } }; if file.write_all(&plist).is_err() { let _ = file.close(); // close error is non-actionable s.set_err(format_args!("Failed to write plist")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + return Self::finish(unsafe { bun_core::heap::take(this) }); } let _ = file.close(); // close error is non-actionable @@ -618,9 +614,9 @@ impl CronRegisterJob { /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. #[cfg(target_os = "macos")] unsafe fn spawn_bootout(this: *mut Self) { - // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. - let s = unsafe { &mut *this }; - s.state = RegisterState::BootingOut; + // SAFETY: shared reborrow; writes go through the interior-mutable fields. + let s = unsafe { &*this }; + s.state.set(RegisterState::BootingOut); let uid_str = match alloc_print_z(format_args!( "gui/{}/bun.cron.{}", get_uid(), @@ -630,7 +626,7 @@ impl CronRegisterJob { Err(_) => { s.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + return Self::finish(unsafe { bun_core::heap::take(this) }); } }; let mut argv: [*const c_char; 4] = [ @@ -647,20 +643,20 @@ impl CronRegisterJob { /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. #[cfg(target_os = "macos")] unsafe fn spawn_bootstrap(this: *mut Self) { - // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. - let s = unsafe { &mut *this }; - s.state = RegisterState::Bootstrapping; - let Some(plist_path) = s.tmp_path.take() else { + // SAFETY: shared reborrow; writes go through the interior-mutable fields. + let s = unsafe { &*this }; + s.state.set(RegisterState::Bootstrapping); + let Some(plist_path) = s.tmp_path.replace(None) else { s.set_err(format_args!("No plist path")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + return Self::finish(unsafe { bun_core::heap::take(this) }); }; let uid_str = match alloc_print_z(format_args!("gui/{}", get_uid())) { Ok(v) => v, Err(_) => { s.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + return Self::finish(unsafe { bun_core::heap::take(this) }); } }; let mut argv: [*const c_char; 5] = [ @@ -777,7 +773,7 @@ pub fn cron_register(global: &JSGlobalObject, frame: &CallFrame) -> JsResult JsResult(), + state: Cell::new(RegisterState::ReadingCrontab), + process: Cell::new(None), + stdout_reader: JsCell::new(OutputReader::init::()), #[cfg(windows)] - stderr_reader: OutputReader::init::(), - remaining_fds: 0, - has_called_process_exit: false, - exit_status: None, - err_msg: None, - tmp_path: None, + stderr_reader: JsCell::new(OutputReader::init::()), + remaining_fds: Cell::new(0), + has_called_process_exit: Cell::new(false), + exit_status: JsCell::new(None), + err_msg: JsCell::new(None), + tmp_path: JsCell::new(None), // SAFETY: `vm_mut().event_loop()` returns the live per-thread `jsc::EventLoop`. event_loop_handle: EventLoopHandle::init(vm_mut().event_loop().cast::<()>()), - })); - let promise_value = { - // SAFETY: just allocated; unique. Short-lived borrow ends before - // `start_*` (which may free `job`). - let job_ref = unsafe { &mut *job }; - job_ref.poll.ref_(bun_io::js_vm_ctx()); - job_ref.promise.value() - }; + }); + job_box.poll.ref_(bun_io::js_vm_ctx()); + let promise_value = job_box.promise.value(); + let job = bun_core::heap::into_raw(job_box); // SAFETY: `job` is the freshly-leaked Box; `start_*` consumes it on // synchronous failure or hands it to the event loop on success. @@ -833,9 +825,9 @@ impl CronRegisterJob { /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. unsafe fn start_windows(this: *mut Self) { - // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. - let s = unsafe { &mut *this }; - s.state = RegisterState::InstallingCrontab; + // SAFETY: shared reborrow; writes go through the interior-mutable fields. + let s = unsafe { &*this }; + s.state.set(RegisterState::InstallingCrontab); let task_name = match alloc_print_z(format_args!( "bun-cron-{}", @@ -845,7 +837,7 @@ impl CronRegisterJob { Err(_) => { s.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + return Self::finish(unsafe { bun_core::heap::take(this) }); } }; @@ -866,7 +858,7 @@ impl CronRegisterJob { s.set_err(format_args!("Failed to build task XML")); } // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + return Self::finish(unsafe { bun_core::heap::take(this) }); } }; @@ -875,15 +867,15 @@ impl CronRegisterJob { Err(_) => { s.set_err(format_args!("Out of memory")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + return Self::finish(unsafe { bun_core::heap::take(this) }); } }; let xml_path_ptr = xml_path.as_ptr(); - s.tmp_path = Some(xml_path); + s.tmp_path.set(Some(xml_path)); let file = match File::openat( Fd::cwd(), - s.tmp_path.as_ref().unwrap(), + s.tmp_path.get().as_ref().unwrap(), sys::O::WRONLY | sys::O::CREAT | sys::O::EXCL, 0o600, ) { @@ -891,14 +883,14 @@ impl CronRegisterJob { Err(_) => { s.set_err(format_args!("Failed to create temp XML file")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + return Self::finish(unsafe { bun_core::heap::take(this) }); } }; if file.write_all(&xml).is_err() { let _ = file.close(); // close error is non-actionable s.set_err(format_args!("Failed to write temp XML file")); // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + return Self::finish(unsafe { bun_core::heap::take(this) }); } let _ = file.close(); // close error is non-actionable @@ -929,7 +921,7 @@ impl Drop for CronRegisterJob { Process::deref(proc); } } - if let Some(p) = self.tmp_path.take() { + if let Some(p) = self.tmp_path.replace(None) { let _ = sys::unlink(&p); } // err_msg, abs_path, schedule, title freed via field Drop. @@ -950,17 +942,17 @@ pub struct CronRemoveJob { poll: KeepAlive, title: ZString, - state: RemoveState, + state: Cell, // LIFETIMES.tsv: SHARED — `Process` is intrusively refcounted (`*mut`). - process: Option<*mut Process>, - stdout_reader: OutputReader, + process: Cell>, + stdout_reader: JsCell, #[cfg(windows)] - stderr_reader: OutputReader, - remaining_fds: i8, - has_called_process_exit: bool, - exit_status: Option, - err_msg: Option>, - tmp_path: Option, + stderr_reader: JsCell, + remaining_fds: Cell, + has_called_process_exit: Cell, + exit_status: JsCell>, + err_msg: JsCell>>, + tmp_path: JsCell>, /// Typed enum for the io-layer FilePoll vtable (`bun_io::EventLoopHandle` /// wraps `*const EventLoopHandle`). event_loop_handle: EventLoopHandle, @@ -987,17 +979,17 @@ bun_io::impl_buffered_reader_parent! { } impl CronJobBase for CronRemoveJob { - fn remaining_fds_mut(&mut self) -> &mut i8 { - &mut self.remaining_fds + fn remaining_fds_mut(&self) -> &Cell { + &self.remaining_fds } - fn err_msg_mut(&mut self) -> &mut Option> { - &mut self.err_msg + fn err_msg_mut(&self) -> &JsCell>> { + &self.err_msg } - fn has_called_process_exit_mut(&mut self) -> &mut bool { - &mut self.has_called_process_exit + fn has_called_process_exit_mut(&self) -> &Cell { + &self.has_called_process_exit } - fn exit_status_mut(&mut self) -> &mut Option { - &mut self.exit_status + fn exit_status_mut(&self) -> &JsCell> { + &self.exit_status } unsafe fn maybe_finished(this: *mut Self) { // SAFETY: caller guarantees `this` is the live heap job with no active borrows. @@ -1006,20 +998,20 @@ impl CronJobBase for CronRemoveJob { } impl CronRemoveJob { - fn set_err(&mut self, args: core::fmt::Arguments<'_>) { - if self.err_msg.is_none() { + fn set_err(&self, args: core::fmt::Arguments<'_>) { + if self.err_msg.get().is_none() { let mut msg = Vec::new(); let _ = msg.write_fmt(args); - self.err_msg = Some(msg); + self.err_msg.set(Some(msg)); } } /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. unsafe fn maybe_finished(this: *mut Self) { - // SAFETY: local reborrow (no FnEntry protector); not used after any - // call below that may free `this`. - let s = unsafe { &mut *this }; - if !s.has_called_process_exit || s.remaining_fds != 0 { + // SAFETY: shared reborrow of the live heap job; every write goes + // through an interior-mutable field, so no `&mut Self` is ever live. + let s = unsafe { &*this }; + if !s.has_called_process_exit.get() || s.remaining_fds.get() != 0 { return; } if let Some(proc) = s.process.take() { @@ -1029,30 +1021,29 @@ impl CronRemoveJob { Process::deref(proc); } } - if s.err_msg.is_some() { - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + if s.err_msg.get().is_some() { + // SAFETY: `this` is the unique live heap job. + return Self::finish(unsafe { bun_core::heap::take(this) }); } - let Some(status) = s.exit_status.take() else { + let Some(status) = s.exit_status.replace(None) else { return; }; match status { Status::Exited(exited) => { - let is_acceptable_nonzero = (s.state == RemoveState::ReadingCrontab + let is_acceptable_nonzero = (s.state.get() == RemoveState::ReadingCrontab && exited.code == 1) - || s.state == RemoveState::BootingOut + || s.state.get() == RemoveState::BootingOut // On Windows, schtasks /delete exits non-zero when the task doesn't exist; // removal of a non-existent job should resolve without error. - || (cfg!(windows) && s.state == RemoveState::InstallingCrontab); + || (cfg!(windows) && s.state.get() == RemoveState::InstallingCrontab); if exited.code != 0 && !is_acceptable_nonzero { - // Owned copy: `final_buffer()` is `&mut self` and would - // alias `s.set_err` below. Copy the trimmed bytes out. + // Copy the trimmed bytes out: the `&mut OutputReader` must + // not survive the `set_err` calls below. Reaches no JS. #[cfg(windows)] - let stderr_owned: Vec = bun_core::strings::trim( - s.stderr_reader.final_buffer().as_slice(), - &ASCII_WHITESPACE, - ) - .to_vec(); + let stderr_owned: Vec = s.stderr_reader.with_mut(|r| { + bun_core::strings::trim(r.final_buffer().as_slice(), &ASCII_WHITESPACE) + .to_vec() + }); #[cfg(windows)] let stderr_output: &[u8] = stderr_owned.as_slice(); #[cfg(not(windows))] @@ -1062,15 +1053,15 @@ impl CronRemoveJob { } else { s.set_err(format_args!("Process exited with code {}", exited.code)); } - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + // SAFETY: `this` is the unique live heap job. + return Self::finish(unsafe { bun_core::heap::take(this) }); } } Status::Signaled(sig) => { - if s.state != RemoveState::BootingOut { + if s.state.get() != RemoveState::BootingOut { s.set_err(format_args!("Process killed by signal {}", sig as i32)); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + // SAFETY: `this` is the unique live heap job. + return Self::finish(unsafe { bun_core::heap::take(this) }); } } Status::Err(err) => { @@ -1078,27 +1069,27 @@ impl CronRemoveJob { "Process error: {}", <&'static str>::from(err.get_errno()) )); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + // SAFETY: `this` is the unique live heap job. + return Self::finish(unsafe { bun_core::heap::take(this) }); } Status::Running => return, } - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + // SAFETY: `this` is the live heap job. unsafe { Self::advance_state(this) }; } /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. unsafe fn advance_state(this: *mut Self) { - // SAFETY: local reborrow; last use precedes any self-freeing call. - let s = unsafe { &mut *this }; + // SAFETY: shared reborrow; writes go through the interior-mutable fields. + let s = unsafe { &*this }; #[cfg(target_os = "macos")] { - match s.state { + match s.state.get() { RemoveState::BootingOut => { let Some(home) = env_var::HOME.get() else { s.set_err(format_args!("HOME not set")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + // SAFETY: local reborrow `s` has ended; `this` is the unique live heap job. + return Self::finish(unsafe { bun_core::heap::take(this) }); }; if let Ok(plist_path) = alloc_print_z(format_args!( "{}/Library/LaunchAgents/bun.cron.{}.plist", @@ -1108,64 +1099,62 @@ impl CronRemoveJob { let _ = sys::unlink(&plist_path); } else { s.set_err(format_args!("Out of memory")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + // SAFETY: local reborrow `s` has ended; `this` is the unique live heap job. + return Self::finish(unsafe { bun_core::heap::take(this) }); } - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::finish(this) }; + // SAFETY: local reborrow `s` has ended; `this` is the unique live heap job. + Self::finish(unsafe { bun_core::heap::take(this) }); } _ => { s.set_err(format_args!("Unexpected state")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::finish(this) }; + // SAFETY: local reborrow `s` has ended; `this` is the unique live heap job. + Self::finish(unsafe { bun_core::heap::take(this) }); } } } #[cfg(not(target_os = "macos"))] { - match s.state { - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + match s.state.get() { + // SAFETY: `this` is the live heap job. RemoveState::ReadingCrontab => unsafe { Self::remove_crontab_entry(this) }, - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - RemoveState::InstallingCrontab => unsafe { Self::finish(this) }, + RemoveState::InstallingCrontab => { + // SAFETY: the local reborrow has ended; `this` is the unique live heap job. + Self::finish(unsafe { bun_core::heap::take(this) }) + } _ => { s.set_err(format_args!("Unexpected state")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - unsafe { Self::finish(this) }; + // SAFETY: local reborrow `s` has ended; `this` is the unique live heap job. + Self::finish(unsafe { bun_core::heap::take(this) }); } } } } - /// Consumes and frees `this` (`heap::take`). - unsafe fn finish(this: *mut Self) { - // SAFETY: caller holds the unique Box; consumed below. Local - // reborrow has no FnEntry protector and is not used after the drop. - let this_ref = unsafe { &mut *this }; - this_ref.state = if this_ref.err_msg.is_some() { + /// Consumes and frees `self`. + fn finish(mut self: Box) { + self.state.set(if self.err_msg.get().is_some() { RemoveState::Failed } else { RemoveState::Done - }; - this_ref.poll.unref(bun_io::js_vm_ctx()); + }); + self.poll.unref(bun_io::js_vm_ctx()); let ev = VirtualMachine::get().event_loop_mut(); ev.enter(); - if let Some(msg) = &this_ref.err_msg { - let _ = this_ref.promise.reject_with_async_stack( - &this_ref.global, - Ok(this_ref + // Move the message out first: settling the promise re-enters JS. + let err_msg = self.err_msg.replace(None); + if let Some(msg) = &err_msg { + let _ = self.promise.reject_with_async_stack( + &self.global, + Ok(self .global .create_error_instance(format_args!("{}", bstr::BStr::new(msg)))), ); } else { - let _ = this_ref - .promise - .resolve(&this_ref.global, JSValue::UNDEFINED); + let _ = self.promise.resolve(&self.global, JSValue::UNDEFINED); } // Drop runs INSIDE the enter/exit scope so Process detach/deref and // reader teardown observe the entered event-loop state. - // SAFETY: `this` was created via heap::alloc in cron_remove. - unsafe { drop(bun_core::heap::take(this)) }; + drop(self); ev.exit(); } @@ -1183,15 +1172,16 @@ impl CronRemoveJob { /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. #[cfg(all(not(target_os = "macos"), not(windows)))] unsafe fn start_linux(this: *mut Self) { - // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. - let s = unsafe { &mut *this }; - s.state = RemoveState::ReadingCrontab; - s.stdout_reader = OutputReader::init::(); - s.stdout_reader.set_parent(this.cast()); + // SAFETY: shared reborrow; writes go through the interior-mutable fields. + let s = unsafe { &*this }; + s.state.set(RemoveState::ReadingCrontab); + s.stdout_reader.set(OutputReader::init::()); + // `set_parent` only stores a pointer — the closure reaches no JS. + s.stdout_reader.with_mut(|r| r.set_parent(this.cast())); let Some(crontab_path) = find_crontab() else { s.set_err(format_args!("crontab not found in PATH")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + // SAFETY: `this` is the unique live heap job. + return Self::finish(unsafe { bun_core::heap::take(this) }); }; let mut argv: [*const c_char; 3] = [crontab_path, c"-l".as_ptr(), core::ptr::null()]; // SAFETY: local reborrow `s` has ended; `this` is the live heap job. @@ -1201,55 +1191,58 @@ impl CronRemoveJob { /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. #[cfg(not(target_os = "macos"))] unsafe fn remove_crontab_entry(this: *mut Self) { - // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. - let s = unsafe { &mut *this }; - let existing_content = s.stdout_reader.final_buffer().as_slice(); + // SAFETY: shared reborrow; writes go through the interior-mutable fields. + let s = unsafe { &*this }; let mut result: Vec = Vec::new(); - if filter_crontab(existing_content, s.title.as_bytes(), &mut result).is_err() { + // `final_buffer()` needs `&mut OutputReader`; the closure reaches no JS. + let filtered = s.stdout_reader.with_mut(|r| { + filter_crontab(r.final_buffer().as_slice(), s.title.as_bytes(), &mut result) + }); + if filtered.is_err() { s.set_err(format_args!("Out of memory")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + // SAFETY: `this` is the unique live heap job. + return Self::finish(unsafe { bun_core::heap::take(this) }); } let tmp_path = match make_temp_path("bun-cron-rm-") { Ok(p) => p, Err(_) => { s.set_err(format_args!("Out of memory")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + // SAFETY: local reborrow `s` has ended; `this` is the unique live heap job. + return Self::finish(unsafe { bun_core::heap::take(this) }); } }; let tmp_path_ptr = tmp_path.as_ptr(); - s.tmp_path = Some(tmp_path); + s.tmp_path.set(Some(tmp_path)); let file = match File::openat( Fd::cwd(), - s.tmp_path.as_ref().unwrap(), + s.tmp_path.get().as_ref().unwrap(), sys::O::WRONLY | sys::O::CREAT | sys::O::EXCL, 0o600, ) { Ok(f) => f, Err(_) => { s.set_err(format_args!("Failed to create temp file")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + // SAFETY: `this` is the unique live heap job. + return Self::finish(unsafe { bun_core::heap::take(this) }); } }; if file.write_all(&result).is_err() { let _ = file.close(); // close error is non-actionable s.set_err(format_args!("Failed to write temp file")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + // SAFETY: `this` is the unique live heap job. + return Self::finish(unsafe { bun_core::heap::take(this) }); } let _ = file.close(); // close error is non-actionable - s.state = RemoveState::InstallingCrontab; - s.stdout_reader = OutputReader::init::(); + s.state.set(RemoveState::InstallingCrontab); + s.stdout_reader.set(OutputReader::init::()); let Some(crontab_path) = find_crontab() else { s.set_err(format_args!("crontab not found in PATH")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + // SAFETY: local reborrow `s` has ended; `this` is the unique live heap job. + return Self::finish(unsafe { bun_core::heap::take(this) }); }; let mut argv: [*const c_char; 3] = [crontab_path, tmp_path_ptr.cast(), core::ptr::null()]; // SAFETY: local reborrow `s` has ended; `this` is the live heap job. @@ -1259,9 +1252,9 @@ impl CronRemoveJob { /// May free `this`. Raw-ptr receiver: see [`CronJobBase`] note. #[cfg(target_os = "macos")] unsafe fn start_mac(this: *mut Self) { - // SAFETY: local reborrow; not used after `spawn_cmd`/`finish`. - let s = unsafe { &mut *this }; - s.state = RemoveState::BootingOut; + // SAFETY: shared reborrow; writes go through the interior-mutable fields. + let s = unsafe { &*this }; + s.state.set(RemoveState::BootingOut); let uid_str = match alloc_print_z(format_args!( "gui/{}/bun.cron.{}", get_uid(), @@ -1270,8 +1263,8 @@ impl CronRemoveJob { Ok(v) => v, Err(_) => { s.set_err(format_args!("Out of memory")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + // SAFETY: local reborrow `s` has ended; `this` is the unique live heap job. + return Self::finish(unsafe { bun_core::heap::take(this) }); } }; let mut argv: [*const c_char; 4] = [ @@ -1304,31 +1297,27 @@ pub fn cron_remove(global: &JSGlobalObject, frame: &CallFrame) -> JsResult(), + state: Cell::new(RemoveState::ReadingCrontab), + process: Cell::new(None), + stdout_reader: JsCell::new(OutputReader::init::()), #[cfg(windows)] - stderr_reader: OutputReader::init::(), - remaining_fds: 0, - has_called_process_exit: false, - exit_status: None, - err_msg: None, - tmp_path: None, + stderr_reader: JsCell::new(OutputReader::init::()), + remaining_fds: Cell::new(0), + has_called_process_exit: Cell::new(false), + exit_status: JsCell::new(None), + err_msg: JsCell::new(None), + tmp_path: JsCell::new(None), // SAFETY: `vm_mut().event_loop()` returns the live per-thread `jsc::EventLoop`. event_loop_handle: EventLoopHandle::init(vm_mut().event_loop().cast::<()>()), - })); - let promise_value = { - // SAFETY: just allocated; unique. Short-lived borrow ends before - // `start_*` (which may free `job`). - let job_ref = unsafe { &mut *job }; - job_ref.poll.ref_(bun_io::js_vm_ctx()); - job_ref.promise.value() - }; + }); + job_box.poll.ref_(bun_io::js_vm_ctx()); + let promise_value = job_box.promise.value(); + let job = bun_core::heap::into_raw(job_box); // SAFETY: `job` is the freshly-leaked Box; `start_*` consumes it on // synchronous failure or hands it to the event loop on success. #[cfg(target_os = "macos")] @@ -1351,9 +1340,9 @@ pub fn cron_remove(global: &JSGlobalObject, frame: &CallFrame) -> JsResult v, Err(_) => { s.set_err(format_args!("Out of memory")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. - return unsafe { Self::finish(this) }; + // SAFETY: local reborrow `s` has ended; `this` is the unique live heap job. + return Self::finish(unsafe { bun_core::heap::take(this) }); } }; let mut argv: [*const c_char; 6] = [ @@ -1388,7 +1377,7 @@ impl Drop for CronRemoveJob { Process::deref(proc); } } - if let Some(p) = self.tmp_path.take() { + if let Some(p) = self.tmp_path.replace(None) { let _ = sys::unlink(&p); } } @@ -1406,7 +1395,6 @@ impl Drop for CronRemoveJob { // + `UnsafeCell`-backed fields suppresses `noalias` on the receiver. #[bun_jsc::JsClass(no_constructor)] #[derive(bun_ptr::CellRefCounted)] -#[ref_count(destroy = Self::destroy_impl)] pub struct CronJob { // bun.ptr.RefCount(...) intrusive — keep raw count for IntrusiveRc compat. ref_count: Cell, @@ -1451,23 +1439,6 @@ pub enum ClearMode { /// RAII owner for one intrusive refcount on a [`CronJob`]. type CronJobDerefOnDrop = bun_ptr::ScopedRef; -impl CronJob { - /// `CellRefCounted::destroy` target (refcount hit zero). - /// - /// Safe fn: only reachable via the `#[ref_count(destroy = …)]` derive, - /// whose generated trait `destroy` upholds the sole-owner contract. - fn destroy_impl(this: *mut Self) { - // deinit: this_value.deinit() then destroy. - // SAFETY: last ref; nobody else holds a pointer. - // Note: `JsRef::deinit()` was dropped — Strong's Drop on - // reassignment handles teardown (JSRef.rs trailer). - unsafe { - (*this).this_value.set(JsRef::empty()); - drop(bun_core::heap::take(this)); - } - } -} - impl CronJob { /// `#[JsClass]` requires a `constructor`; the JS class is not directly /// constructible (`noConstructor` in .classes.ts) so this always throws. @@ -2065,15 +2036,15 @@ pub fn cron_parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult); + fn set_err(&self, args: core::fmt::Arguments<'_>); /// Consumes and frees `this`. unsafe fn finish(this: *mut Self); - fn process_slot(&mut self) -> &mut Option<*mut Process>; + fn process_slot(&self) -> &Cell>; #[cfg(unix)] - fn stdout_reader(&mut self) -> &mut OutputReader; + fn stdout_reader(&self) -> &JsCell; #[cfg(windows)] - fn stderr_reader(&mut self) -> &mut OutputReader; - fn remaining_fds(&mut self) -> &mut i8; + fn stderr_reader(&self) -> &JsCell; + fn remaining_fds(&self) -> &Cell; } bun_spawn::link_impl_ProcessExit! { @@ -2092,50 +2063,50 @@ bun_spawn::link_impl_ProcessExit! { impl SpawnCmdTarget for CronRegisterJob { const EXIT_KIND: bun_spawn::ProcessExitKind = bun_spawn::ProcessExitKind::CronRegister; - fn set_err(&mut self, args: core::fmt::Arguments<'_>) { + fn set_err(&self, args: core::fmt::Arguments<'_>) { CronRegisterJob::set_err(self, args) } unsafe fn finish(this: *mut Self) { // SAFETY: caller guarantees `this` is the live heap job with no active borrows. - unsafe { CronRegisterJob::finish(this) } + unsafe { CronRegisterJob::finish(bun_core::heap::take(this)) } } - fn process_slot(&mut self) -> &mut Option<*mut Process> { - &mut self.process + fn process_slot(&self) -> &Cell> { + &self.process } #[cfg(unix)] - fn stdout_reader(&mut self) -> &mut OutputReader { - &mut self.stdout_reader + fn stdout_reader(&self) -> &JsCell { + &self.stdout_reader } #[cfg(windows)] - fn stderr_reader(&mut self) -> &mut OutputReader { - &mut self.stderr_reader + fn stderr_reader(&self) -> &JsCell { + &self.stderr_reader } - fn remaining_fds(&mut self) -> &mut i8 { - &mut self.remaining_fds + fn remaining_fds(&self) -> &Cell { + &self.remaining_fds } } impl SpawnCmdTarget for CronRemoveJob { const EXIT_KIND: bun_spawn::ProcessExitKind = bun_spawn::ProcessExitKind::CronRemove; - fn set_err(&mut self, args: core::fmt::Arguments<'_>) { + fn set_err(&self, args: core::fmt::Arguments<'_>) { CronRemoveJob::set_err(self, args) } unsafe fn finish(this: *mut Self) { - // SAFETY: caller guarantees `this` is the live heap job with no active borrows. - unsafe { CronRemoveJob::finish(this) } + // SAFETY: caller guarantees `this` is the unique live heap job. + CronRemoveJob::finish(unsafe { bun_core::heap::take(this) }) } - fn process_slot(&mut self) -> &mut Option<*mut Process> { - &mut self.process + fn process_slot(&self) -> &Cell> { + &self.process } #[cfg(unix)] - fn stdout_reader(&mut self) -> &mut OutputReader { - &mut self.stdout_reader + fn stdout_reader(&self) -> &JsCell { + &self.stdout_reader } #[cfg(windows)] - fn stderr_reader(&mut self) -> &mut OutputReader { - &mut self.stderr_reader + fn stderr_reader(&self) -> &JsCell { + &self.stderr_reader } - fn remaining_fds(&mut self) -> &mut i8 { - &mut self.remaining_fds + fn remaining_fds(&self) -> &Cell { + &self.remaining_fds } } @@ -2151,12 +2122,12 @@ unsafe fn spawn_cmd_generic( stdin_opt: spawn::Stdio, stdout_opt: spawn::Stdio, ) { - // SAFETY: local reborrow (no FnEntry protector). Re-derived after each - // section so no `&mut T` outlives a potentially-freeing call. - let s = unsafe { &mut *this }; - *s.has_called_process_exit_mut() = false; - *s.exit_status_mut() = None; - *s.remaining_fds() = 0; + // SAFETY: shared reborrow — no `&mut T` exists at all, so nothing can + // outlive the potentially-freeing / re-entrant calls below. + let s = unsafe { &*this }; + s.has_called_process_exit_mut().set(false); + s.exit_status_mut().set(None); + s.remaining_fds().set(0); #[cfg(not(windows))] let resolved_argv0: Option<*const c_char> = None; @@ -2287,30 +2258,35 @@ unsafe fn spawn_cmd_generic( if let Some(stdout) = spawned.stdout { let this_ptr = this.cast::(); if !spawned.memfds[1] { - s.stdout_reader().set_parent(this_ptr); + // Each closure holds `&mut OutputReader` for exactly one io + // call; none of them can re-enter JS or `this`. + s.stdout_reader().with_mut(|r| r.set_parent(this_ptr)); let _ = sys::set_nonblocking(stdout); - *s.remaining_fds() += 1; - { + s.remaining_fds().set(s.remaining_fds().get() + 1); + s.stdout_reader().with_mut(|r| { use bun_io::pipe_reader::PosixFlags; - let flags = &mut s.stdout_reader().flags; + let flags = &mut r.flags; flags.insert(PosixFlags::NONBLOCKING | PosixFlags::SOCKET); flags.remove( PosixFlags::MEMFD | PosixFlags::RECEIVED_EOF | PosixFlags::CLOSED_WITHOUT_REPORTING, ); - } - if s.stdout_reader().start(stdout, true).is_err() { + }); + if s.stdout_reader() + .with_mut(|r| r.start(stdout, true)) + .is_err() + { s.set_err(format_args!("Failed to start reading stdout")); - // SAFETY: local reborrow `s` has ended; `this` is the live heap job. + // SAFETY: `this` is the live heap job. return unsafe { T::finish(this) }; } - if let Some(p) = s.stdout_reader().handle.get_poll() { + if let Some(p) = s.stdout_reader().get().handle.get_poll() { p.set_flag(bun_io::FilePollFlag::Socket); } } else { - s.stdout_reader().set_parent(this_ptr); - s.stdout_reader().start_memfd(stdout); + s.stdout_reader().with_mut(|r| r.set_parent(this_ptr)); + s.stdout_reader().with_mut(|r| r.start_memfd(stdout)); } } } @@ -2327,11 +2303,16 @@ unsafe fn spawn_cmd_generic( // callback + double-free on reader close). if let spawn::WindowsStdioResult::Buffer(pipe) = spawned.stderr.take() { debug_assert!(core::ptr::eq(Box::as_ref(&pipe), stderr_pipe_ptr)); - s.stderr_reader().source = Some(bun_io::Source::Pipe(pipe)); + // One io call per closure; none of them re-enter JS or `this`. + s.stderr_reader() + .with_mut(|r| r.source = Some(bun_io::Source::Pipe(pipe))); s.stderr_reader() - .set_parent(this.cast::()); - *s.remaining_fds() += 1; - if s.stderr_reader().start_with_current_pipe().is_err() { + .with_mut(|r| r.set_parent(this.cast::())); + s.remaining_fds().set(s.remaining_fds().get() + 1); + if s.stderr_reader() + .with_mut(|r| r.start_with_current_pipe()) + .is_err() + { s.set_err(format_args!("Failed to start reading stderr")); return unsafe { T::finish(this) }; } @@ -2341,7 +2322,7 @@ unsafe fn spawn_cmd_generic( // SAFETY: `vm_mut().event_loop()` returns the live per-thread `jsc::EventLoop`. let ev_handle = EventLoopHandle::init(vm_mut().event_loop().cast::<()>()); let process = spawned.to_process(ev_handle, false); - *s.process_slot() = Some(process); + s.process_slot().set(Some(process)); // SAFETY: `process` was just allocated by `to_process`; we hold the only // ref. `this` is the owning `Box` (only freed in `T::finish`, gated on // `has_called_process_exit`), so it outlives `process`. diff --git a/src/runtime/api/filesystem_router.rs b/src/runtime/api/filesystem_router.rs index d39d6d12d922..3a83df91411d 100644 --- a/src/runtime/api/filesystem_router.rs +++ b/src/runtime/api/filesystem_router.rs @@ -77,9 +77,8 @@ impl<'a, 'r> Router::ResolverLike for RouterResolver<'a, 'r> { Fs::FileSystem::instance() } #[inline] - fn fs_impl(&self) -> *mut Fs::Implementation { - // SAFETY: `&fs.fs` — the `Implementation` field of the singleton. - unsafe { &raw mut (*self.0.fs()).fs } + fn fs_impl(&self) -> bun_ptr::ParentRef { + bun_ptr::ParentRef::new(&self.0.fs_ref().fs) } #[inline] fn read_dir_info_ignore_error(&mut self, path: &[u8]) -> Option { @@ -408,16 +407,14 @@ impl FileSystemRouter { continue 'outer; } // `Transpiler::fs_mut()` is the audited safe `&mut FileSystem` - // accessor for the process-lifetime singleton; `&mut .fs` (the - // `Implementation` field) is the lazy-stat receiver. `kind` - // needs `&mut Entry` to update the cached stat; no shared - // borrow of `*entry_ptr` is live across this block. + // accessor for the process-lifetime singleton; `.fs` (the + // `Implementation` field) is the lazy-stat receiver. let kind = { - let fs_impl = &mut vm.transpiler.fs_mut().fs; + let fs_impl = &vm.transpiler.fs_mut().fs; // SAFETY: `entry_ptr` is a live `*mut Entry` in the process-static // EntryStore (checked non-null above); the lazy-stat rewrite is - // serialized on `Entry.mutex`; fs_impl is the process-global RealFS. - unsafe { (&*entry_ptr).kind(fs_impl, false) } + // serialized on `Entry.mutex`. + unsafe { &*entry_ptr }.kind(bun_ptr::ParentRef::new(fs_impl), false) }; if kind == Fs::EntryKind::Dir { for banned_dir in Router::BANNED_DIRS.iter() { @@ -425,7 +422,7 @@ impl FileSystemRouter { continue 'outer; } } - let abs_parts: [&[u8]; 2] = [entry.dir, entry.base()]; + let abs_parts: [&[u8]; 2] = [entry.dir(), entry.base()]; // `abs()` writes into a thread-local buffer; copy out // before recursing (recursion overwrites it). let full_path = vm.fs().abs(&abs_parts).to_vec(); @@ -845,31 +842,28 @@ impl MatchedRoute { // Note: `deinit` is called only from `finalize`; not exposed as `Drop` because // `MatchedRoute` is a JsClass m_ctx payload (finalize owns teardown per PORTING.md). - fn deinit(this: *mut MatchedRoute) { - // SAFETY: called from finalize on mutator thread. - let this_ref = unsafe { &mut *this }; - this_ref.query_string_map.set(None); - this_ref.param_map.set(None); - if this_ref.needs_deinit { + fn deinit(mut this: Box) { + this.query_string_map.set(None); + this.param_map.set(None); + if this.needs_deinit { // We own the `path` allocation from `match` as // `pathname_backing`; dropping it (and `params_list_holder`) here releases the // borrowed bytes BEFORE `route_holder`'s slices would dangle on Box drop. - this_ref.pathname_backing = ZigStringSlice::EMPTY; - *this_ref.params_list_holder.get_mut() = route_param::List::default(); + this.pathname_backing = ZigStringSlice::EMPTY; + *this.params_list_holder.get_mut() = route_param::List::default(); } - if let Some(p) = this_ref.origin.take() { + if let Some(p) = this.origin.take() { p.get().deref(); } - if let Some(p) = this_ref.asset_prefix.take() { + if let Some(p) = this.asset_prefix.take() { p.get().deref(); } - if let Some(p) = this_ref.base_dir.take() { + if let Some(p) = this.base_dir.take() { p.get().deref(); } - // SAFETY: `this` was heap-allocated by codegen at construction. - drop(unsafe { bun_core::heap::take(this) }); + drop(this); } #[bun_jsc::host_fn(getter)] @@ -878,9 +872,7 @@ impl MatchedRoute { } pub fn finalize(self: Box) { - // `deinit` frees the allocation itself; hand ownership back so its - // existing raw-ptr teardown path stays intact. - Self::deinit(Box::into_raw(self)); + Self::deinit(self); } #[bun_jsc::host_fn(getter)] diff --git a/src/runtime/api/html_rewriter.rs b/src/runtime/api/html_rewriter.rs index 97c5caa4e326..1e5845eb9d3c 100644 --- a/src/runtime/api/html_rewriter.rs +++ b/src/runtime/api/html_rewriter.rs @@ -16,7 +16,6 @@ use bun_jsc::{ // owner of the `on_quiet_unhandled_rejection_handler_capture_value` assoc fn. use bun_jsc::virtual_machine::VirtualMachine; -use crate::webcore::response::HeadersRef; use crate::webcore::{self, Response}; use bun_core::String as BunString; // `ZigString` re-exports `bun_core::ZigString`; JSC-side methods @@ -445,23 +444,22 @@ impl HTMLRewriter { if kind != ResponseKind::Other { let body_value = webcore::body::extract(global, response_value)?; - let resp = bun_core::heap::into_raw(Box::new(Response::init( - webcore::response::Init { - status_code: 200, - ..Default::default() - }, - body_value, - BunString::empty(), - false, - ))); - let _resp_guard = scopeguard::guard(resp, |r| { - // SAFETY: `r` is the `heap::into_raw` allocation from just - // above; finalize takes ownership and frees it exactly once. - Response::finalize(unsafe { Box::from_raw(r) }) - }); + // Owned `Box`; the guard hands it to `finalize` (which releases the + // intrusive +1) on every exit path, including `?`. + let mut resp = scopeguard::guard( + Box::new(Response::init( + webcore::response::Init { + status_code: 200, + ..Default::default() + }, + body_value, + BunString::empty(), + false, + )), + Response::finalize, + ); - // SAFETY: `resp` is a live `heap::into_raw` allocation, never null. - let out_response_value = self.begin_transform(global, unsafe { &mut *resp })?; + let out_response_value = self.begin_transform(global, &mut resp)?; // Check if the returned value is an error and throw it properly if let Some(err) = out_response_value.to_error() { return Err(global.throw_value(err)); @@ -547,10 +545,10 @@ pub struct BufferOutputSink { // Intrusive RefCount; *Self is the `SinkRef` carried inside `rewriter`. ref_count: Cell, pub global: GlobalRef, // JSC_BORROW - pub bytes: MutableString, + pub bytes: JsCell, // Heap-allocated (never held by value): `run_output_sink` must reach the // rewriter through a raw pointer, never a `&mut` of `*sink`, because the - // output sink re-enters `&mut *sink` while the rewriter runs. + // output sink re-enters `&*sink` while the rewriter runs. pub rewriter: *mut lol_html::HtmlRewriter<'static, SinkRef>, // null when unset pub context: Rc>, pub response: *mut Response, // BORROW_FIELD: kept alive by response_value Strong @@ -591,7 +589,7 @@ impl BufferOutputSink { let sink = bun_core::heap::into_raw(Box::new(BufferOutputSink { ref_count: Cell::new(1), global: GlobalRef::from(global), - bytes: MutableString::init_empty(), + bytes: JsCell::new(MutableString::init_empty()), rewriter: core::ptr::null_mut(), context, response: core::ptr::null_mut(), @@ -642,7 +640,7 @@ impl BufferOutputSink { // caller. let scope = vm.unhandled_rejection_scope(); let prev_unhandled_pending_rejection_to_capture = vm.unhandled_pending_rejection_to_capture; - vm.unhandled_pending_rejection_to_capture = Some(sink_error_ptr); + vm.unhandled_pending_rejection_to_capture = Some(NonNull::from(&sink_error)); // SAFETY: sink is a live heap allocation (refcount >= 1); sink_error_ptr // is non-null (addr of stack local). unsafe { (*sink).tmp_sync_error = Some(NonNull::new_unchecked(sink_error_ptr)) }; @@ -686,7 +684,9 @@ impl BufferOutputSink { enable_esi_tags: false, adjust_charset_on_meta_tag: false, }, - SinkRef(sink), + // SAFETY: `sink` is the `heap::into_raw` root (refcount >= 1) and + // outlives the rewriter that is stored back onto it below. + SinkRef(unsafe { bun_ptr::ParentRef::from_raw_mut(sink) }), ))); // SAFETY: sink is a live heap allocation (refcount >= 1). unsafe { (*sink).rewriter = rewriter }; @@ -701,12 +701,9 @@ impl BufferOutputSink { ); // https://github.com/oven-sh/bun/issues/3334 - // Note: `clone_this` takes `&mut self`, so use the `_mut` - // accessor (original is `*mut Response`). `clone_this` only reads - // `self` (FFI mutates a freshly-allocated clone, not the receiver). - if let Some(headers) = (*original).get_init_headers_mut() { + if let Some(headers) = (*original).headers() { let cloned = headers.clone_this(global)?; - (*result).set_init_headers(cloned.map(|p| HeadersRef::adopt(p))); + (*result).set_init_headers(cloned); } } @@ -885,7 +882,9 @@ impl BufferOutputSink { // invariant). Read fields into locals before the rewriter calls so no // borrow of `*sink` is live across the re-entrant output sink. let (global, response, rewriter) = unsafe { - let _ = (*sink).bytes.grow_by(bytes.len()); // OOM/capacity: fire-and-forget + (*sink).bytes.with_mut(|b| { + let _ = b.grow_by(bytes.len()); // OOM/capacity: fire-and-forget + }); ((*sink).global, (*sink).response, (*sink).rewriter) }; @@ -928,38 +927,40 @@ impl BufferOutputSink { None } - pub fn done(&mut self) { + pub fn done(&self) { + // Take the buffer out of the cell before `resolve` below reaches JS. + let list = self.bytes.replace(MutableString::init_empty()).list; + let (response, global) = (self.response, self.global); // SAFETY: self.response is kept alive by self.response_value (Strong // root) for the lifetime of this sink. - let body_value = unsafe { (*self.response).get_body_value() }; + let body_value = unsafe { (*response).get_body_value() }; let mut prev_value = core::mem::replace( body_value, webcore::body::Value::InternalBlob(webcore::InternalBlob { - bytes: core::mem::replace(&mut self.bytes, MutableString::init_empty()).list, + bytes: list, was_string: false, }), ); - let _ = webcore::body::Value::resolve(&mut prev_value, body_value, &self.global, None); + let _ = webcore::body::Value::resolve(&mut prev_value, body_value, &global, None); // TODO: properly propagate exception upwards } - pub fn write(&mut self, bytes: &[u8]) { - let _ = self.bytes.append(bytes); // OOM/capacity: fire-and-forget + pub fn write(&self, bytes: &[u8]) { + self.bytes.with_mut(|b| { + let _ = b.append(bytes); // OOM/capacity: fire-and-forget + }); } } /// `lol_html::OutputSink` for the rewriter built in [`BufferOutputSink::init`]. -/// Carries a raw `*mut BufferOutputSink` (never a reference) so the rewriter -/// stored on the sink does not self-borrow. -pub struct SinkRef(*mut BufferOutputSink); +/// Non-owning back-pointer to the sink that owns the rewriter; `handle_chunk` +/// only ever forms `&BufferOutputSink`, so it cannot alias the raw writers. +pub struct SinkRef(bun_ptr::ParentRef); impl lol_html::OutputSink for SinkRef { fn handle_chunk(&mut self, chunk: &[u8]) { - // SAFETY: `self.0` is the sink that owns this rewriter (refcount > 0 - // inside `run_output_sink`), and no other `&mut *sink` is live — - // `run_output_sink` reads its fields into locals before the call. - let sink = unsafe { &mut *self.0 }; + let sink = self.0.get(); // lol-html signals end-of-output with a zero-length final chunk. if chunk.is_empty() { sink.done(); @@ -1270,8 +1271,9 @@ where // mechanism if it's available (this is the same mechanism used // by BufferOutputSink) if let Some(err_ptr) = vm().unhandled_pending_rejection_to_capture { - // SAFETY: VM-owned pointer set by BufferOutputSink::init. - unsafe { *err_ptr = exc_value }; + // SAFETY: points at the live `Cell` stack local of + // the frame that installed the capture slot. + unsafe { err_ptr.as_ref() }.set(exc_value); exc_value.protect(); } } @@ -1288,8 +1290,9 @@ where let exc_value = JSValue::from_cell(exc.as_ptr()); // Store the exception in the VM's unhandled rejection capture mechanism if let Some(err_ptr) = vm().unhandled_pending_rejection_to_capture { - // SAFETY: VM-owned pointer set by BufferOutputSink::init. - unsafe { *err_ptr = exc_value }; + // SAFETY: points at the live `Cell` stack local of the + // frame that installed the capture slot. + unsafe { err_ptr.as_ref() }.set(exc_value); exc_value.protect(); } // Clear the exception to prevent assertion failures @@ -1427,12 +1430,13 @@ fn create_lolhtml_error(global: &JSGlobalObject, message: &dyn core::fmt::Displa // SAFETY: bun_vm() returns the live VM raw ptr; VM outlives this call. let vm: &VirtualMachine = global.bun_vm(); if let Some(err_ptr) = vm.unhandled_pending_rejection_to_capture { - // SAFETY: VM-owned pointer; valid while VM lives. - let slot = unsafe { &mut *err_ptr }; - if !slot.is_empty() { + // SAFETY: points at the live `Cell` stack local of the frame + // that installed the capture slot; `Cell` hands out no reference. + let slot = unsafe { err_ptr.as_ref() }; + let result = slot.get(); + if !result.is_empty() { // it's a promise rejection - let result = *slot; - *slot = JSValue::ZERO; + slot.set(JSValue::ZERO); return result; } } @@ -1820,7 +1824,6 @@ type RawAttributeIterator = core::slice::Iter<'static, lol_html::html_content::A #[bun_jsc::JsClass(no_construct, no_finalize, no_constructor)] #[derive(bun_ptr::CellRefCounted)] -#[ref_count(destroy = AttributeIterator::destroy_on_zero)] pub struct AttributeIterator { // Intrusive RefCount; *Self is the JS wrapper m_ctx. ref_count: Cell, @@ -1828,22 +1831,17 @@ pub struct AttributeIterator { pub iterator: Cell<*mut RawAttributeIterator>, } +/// Runs from `CellRefCounted::destroy`'s default (`drop(Box::from_raw(this))`) +/// when the refcount hits zero. `detach()` is idempotent. +impl Drop for AttributeIterator { + fn drop(&mut self) { + self.detach(); + } +} + impl AttributeIterator { // `ref_()`/`deref()` provided by `#[derive(CellRefCounted)]`. - /// `CellRefCounted::destroy` target — detach the lol-html iterator before - /// freeing the Box. - /// - /// Safe fn: only reachable via the `#[ref_count(destroy = …)]` derive, - /// whose generated trait `destroy` upholds the sole-owner contract. - fn destroy_on_zero(this: *mut Self) { - // SAFETY: refcount hit zero; sole owner of a `heap::alloc`'d `Self`. - unsafe { - (*this).detach(); - drop(bun_core::heap::take(this)); - } - } - fn detach(&self) { let iterator = self.iterator.replace(core::ptr::null_mut()); if !iterator.is_null() { @@ -1911,7 +1909,6 @@ impl AttributeIterator { #[bun_jsc::JsClass(no_construct, no_finalize, no_constructor)] #[derive(bun_ptr::CellRefCounted)] -#[ref_count(destroy = Element::destroy_on_zero)] pub struct Element { // Intrusive RefCount; *Self is the JS wrapper m_ctx. ref_count: Cell, @@ -1928,22 +1925,18 @@ pub struct Element { pub attribute_iterators: JsCell>, } +/// Detach borrowed sub-objects before the fields are freed. Runs from the +/// derived `CellRefCounted::destroy` at refcount zero; `invalidate()` is +/// idempotent, so the handler-return call at the scopeguard is harmless. +impl Drop for Element { + fn drop(&mut self) { + self.invalidate(); + } +} + impl Element { // `ref_()`/`deref()` provided by `#[derive(CellRefCounted)]`. - /// `CellRefCounted::destroy` target — invalidate borrowed sub-objects - /// before freeing the Box. - /// - /// Safe fn: only reachable via the `#[ref_count(destroy = …)]` derive, - /// whose generated trait `destroy` upholds the sole-owner contract. - fn destroy_on_zero(this: *mut Self) { - // SAFETY: refcount hit zero; sole owner of a `heap::alloc`'d `Self`. - unsafe { - (*this).invalidate(); - drop(bun_core::heap::take(this)); - } - } - pub fn init(element: *mut RawElement) -> *mut Element { bun_core::heap::into_raw(Box::new(Element { ref_count: Cell::new(1), diff --git a/src/runtime/api/js_bundle_completion_task.rs b/src/runtime/api/js_bundle_completion_task.rs index eb4547593e9b..f61fa5338a1d 100644 --- a/src/runtime/api/js_bundle_completion_task.rs +++ b/src/runtime/api/js_bundle_completion_task.rs @@ -52,7 +52,7 @@ use crate::server::html_bundle; /// See module doc for the layering rationale. #[derive(bun_ptr::RefCounted)] -#[ref_count(destroy = Self::deinit, debug_name = "JSBundleCompletionTask")] +#[ref_count(debug_name = "JSBundleCompletionTask")] pub struct JSBundleCompletionTask { // NOTE: this should arguably be a thread-safe refcount, but it is the plain // (non-atomic) `RefCount` — a pre-existing discrepancy. See the @@ -66,7 +66,7 @@ pub struct JSBundleCompletionTask { pub global_this: BackRef, pub promise: jsc::JSPromiseStrong, pub poll_ref: KeepAlive, - pub env: *mut bun_dotenv::Loader<'static>, + pub env: bun_ptr::ParentRef>, pub log: bun_ast::Log, pub cancelled: bool, @@ -82,22 +82,16 @@ pub struct JSBundleCompletionTask { pub started_at_ns: u64, } -impl JSBundleCompletionTask { - /// `RefCounted` destructor — last ref dropped. - /// - /// Safe fn: only reachable via the `#[ref_count(destroy = …)]` derive, - /// whose generated trait `destructor` upholds the sole-owner contract. - fn deinit(this: *mut Self) { - // SAFETY: refcount hit zero; `this` is the sole owner of a - // `heap::alloc`'d allocation. - let mut boxed = unsafe { bun_core::heap::take(this) }; - boxed.poll_ref.disable(); - if let Some(plugin) = boxed.plugins.take() { +/// Runs when the last ref drops: the derived `RefCounted::destructor` reclaims +/// the `heap::into_raw` allocation as a `Box`. +impl Drop for JSBundleCompletionTask { + fn drop(&mut self) { + self.poll_ref.disable(); + if let Some(plugin) = self.plugins.take() { // `plugin` is the live FFI handle stashed at construction; // last-ref drop is the only place that releases it. Plugin::destroy(plugin.as_ptr()); } - // Owned fields (`config`, `log`, `result`, `promise`) drop with the Box. } } @@ -119,7 +113,9 @@ pub(crate) fn create_and_schedule_completion_task( event_loop: *mut EventLoop, ) -> Result<*mut JSBundleCompletionTask, bun_core::Error> { let vm = global_this.bun_vm_ptr(); - let env = global_this.bun_vm().transpiler.env; + // SAFETY: the per-VM dotenv loader is non-null after `Transpiler::init` and + // outlives every completion task; `from_raw_mut` keeps write provenance. + let env = unsafe { bun_ptr::ParentRef::from_raw_mut(global_this.bun_vm().transpiler.env) }; let completion = bun_core::heap::into_raw(Box::new(JSBundleCompletionTask { ref_count: RefCount::init(), config, @@ -211,7 +207,7 @@ impl JSBundleCompletionTask { /// (`to_js_error` / `on_complete_anytask`) stay safe. The plugin is a C++ /// `JSBundlerPlugin` opaque created by [`PluginJscExt::create`] and /// `protect()`-ed for the task's lifetime; it is freed only via - /// `Plugin::destroy` in `deinit` *after* `take()` clears `self.plugins`. + /// `Plugin::destroy` in `Drop` *after* `take()` clears `self.plugins`. /// While the field is `Some` the pointee is therefore live, pinned, and /// disjoint from `*self` (separate C++-heap allocation). #[inline] @@ -396,9 +392,9 @@ impl JSBundleCompletionTask { flags |= StandaloneFlags::DISABLE_AUTOLOAD_PACKAGE_JSON; } - // SAFETY: `self.env` is the per-VM `DotEnv.Loader` stashed at - // construction; valid for the lifetime of the VirtualMachine. - let env = unsafe { &mut *self.env.cast::() }; + // SAFETY: JS-thread exclusive; no other borrow of the loader overlaps + // `to_executable`, which never re-enters JS. + let env = unsafe { self.env.assume_mut() }; let result = match to_executable( &compile_options.compile_target, @@ -1073,23 +1069,15 @@ impl CompletionStruct for JSBundleCompletionTask { }; let log: *mut bun_ast::Log = &raw mut self.log; - // SAFETY: `self.env` is the per-VM dotenv loader stashed at - // construction; cast erases `'_` (bun_dotenv::Loader is invariant on - // its arena lifetime, but `Transpiler::init` only stores the pointer). - let env = self.env.cast::>(); + // `Transpiler::init` only stores the pointer; `as_mut_ptr` preserves the + // write provenance it was constructed with. + let env = self.env.as_mut_ptr(); let t = Transpiler::init(bump, log, opts, Some(env))?; let transpiler: &'a mut Transpiler<'a> = bump.alloc(t); // Post-init field wiring. - // Reborrow through a raw ptr so `&mut self` is usable - // again after handing `&'a mut Transpiler` (which is tied to `bump`, - // not `self`) to the trait method. - let tp: *mut Transpiler<'a> = transpiler; - // SAFETY: `tp` aliases nothing in `self`; lives in `bump`. - self.configure_bundler(unsafe { &mut *tp }, bump)?; - // SAFETY: `tp` was the unique `&'a mut` slot from `bump.alloc`; the - // reborrow above has ended. - Ok(unsafe { &mut *tp }) + self.configure_bundler(transpiler, bump)?; + Ok(transpiler) } fn init_and_run<'a>( diff --git a/src/runtime/api/output_file_jsc.rs b/src/runtime/api/output_file_jsc.rs index ff6cbd94d19e..614ae91df318 100644 --- a/src/runtime/api/output_file_jsc.rs +++ b/src/runtime/api/output_file_jsc.rs @@ -35,9 +35,7 @@ fn set_blob_mime(blob: &mut Blob, mime: MimeType) { blob.content_type .set(crate::webcore::blob::BlobContentType::from_mime(&mime)); if let Some(store) = blob.store.get().as_ref() { - // SAFETY: `store` is the freshly-allocated backing store uniquely owned - // by `blob`; no other borrow exists yet. - unsafe { (*store.as_ptr()).mime_type = mime }; + store.mime_type.set(mime); } } diff --git a/src/runtime/api/standalone_graph_jsc.rs b/src/runtime/api/standalone_graph_jsc.rs index fb5eb6e1a97f..ef3729c03e9b 100644 --- a/src/runtime/api/standalone_graph_jsc.rs +++ b/src/runtime/api/standalone_graph_jsc.rs @@ -6,7 +6,7 @@ use core::ptr::NonNull; use bun_core::{self as bstring, strings}; use bun_http::MimeType; -use bun_jsc::JSGlobalObject; +use bun_jsc::{JSGlobalObject, JsCell}; // `StandaloneModuleGraph` here is the inner *module* (so // `StandaloneModuleGraph::BASE_PUBLIC_PATH_WITH_DEFAULT_SUFFIX` resolves); @@ -45,7 +45,7 @@ impl FileJsc for File { // forbids partial moves out of the temporary default. let store = StoreRef::from(Store::new(Store { data: Data::Bytes(bytes), - mime_type: MimeType::NONE, + mime_type: JsCell::new(MimeType::NONE), ref_count: bun_ptr::ThreadSafeRefCount::init(), is_all_ascii: None, })); @@ -63,13 +63,12 @@ impl FileJsc for File { bun_paths::extension(self.name), b'.', )) { - // SAFETY: `store_ptr` is the sole live mutable view; held ref - // guarantees liveness for the process lifetime. - let store = unsafe { &mut *store_ptr }; + // SAFETY: held ref guarantees liveness for the process lifetime. + let store = unsafe { &*store_ptr }; b.content_type .set(crate::webcore::blob::BlobContentType::from_mime(&mime)); b.content_type_was_set.set(true); - store.mime_type = mime; + store.mime_type.set(mime); } // The real name goes here: diff --git a/src/runtime/bake/DevServer.rs b/src/runtime/bake/DevServer.rs index 9a52eb4b0805..cafe81c95e22 100644 --- a/src/runtime/bake/DevServer.rs +++ b/src/runtime/bake/DevServer.rs @@ -1920,9 +1920,8 @@ fn on_memory_visualizer_corked(resp: AnyResponse) { } struct RequestEnsureRouteBundledCtx { - // Note: erased to raw pointer — a `&mut DevServer` field would alias the - // caller's borrow. - dev: *mut DevServer, + // Non-owning backref: the DevServer outlives this stack-local ctx. + dev: bun_ptr::ParentRef, req: ReqOrSaved, resp: AnyResponse, kind: deferred_request::HandlerKind, @@ -1930,15 +1929,15 @@ struct RequestEnsureRouteBundledCtx { } impl RequestEnsureRouteBundledCtx { - /// Reborrow the erased `dev` pointer. + /// Exclusive borrow of the parent DevServer. /// # Safety - /// `self.dev` is set from a live `&mut DevServer` at ctx construction and - /// outlives the ctx (the ctx is stack-local in the request handler scope). + /// No other borrow of the DevServer may overlap the returned one; the ctx + /// is stack-local in the request handler scope. #[inline] fn dev_mut(&mut self) -> &mut DevServer { - // SAFETY: `self.dev` was set from a live `&mut DevServer` at ctx - // construction and outlives this stack-local ctx. - unsafe { &mut *self.dev } + // SAFETY: `dev` was built by `from_raw_mut` from a live `&mut DevServer` + // that outlives this stack-local ctx. + unsafe { self.dev.assume_mut() } } fn on_defer(&mut self, bundle_field: BundleQueueType) -> JsResult<()> { @@ -2294,8 +2293,8 @@ impl DevServer { // erase to `c_void` and cast back inside the trampoline. resp.on_aborted( |p: *mut c_void, r: AnyResponse| { - // SAFETY: p is the &mut deferred.data registered below; lifetime erased - unsafe { &mut *p.cast::() }.on_abort(r) + // SAFETY: `p` is the `deferred.data` ctx registered below. + unsafe { bun_ptr::callback_ctx::(p) }.on_abort(r) }, deferred_data_ptr, ); @@ -2342,10 +2341,9 @@ impl DevServer { data: unsafe { ::core::ptr::NonNull::new_unchecked(deferred_data_ptr) }, deref_fn: { fn deref_fn(ptr: *mut c_void) { - // SAFETY: ptr is &mut DeferredRequest from above - let self_: &mut DeferredRequest = - unsafe { &mut *ptr.cast::() }; - self_.weak_deref(); + // SAFETY: `ptr` is the `deferred.data` ctx registered above. + unsafe { bun_ptr::callback_ctx::(ptr) } + .weak_deref(); } deref_fn }, @@ -2817,13 +2815,8 @@ impl DevServer { resp: AnyResponse, method: Method, ) { - // Note: erase `self` to a raw pointer so the `route_bundle` borrow - // doesn't conflict with the `&mut self` calls below. Per docs/PORTING.md §Global mutable state: hold - // `*mut T` and deref per-access; do not bind a long-lived `&mut`. - let self_ptr = std::ptr::from_mut::(self); - // SAFETY: `route_bundles` is not reallocated for the duration of this fn. let route_bundle: *mut RouteBundle = - &raw mut unsafe { &mut *self_ptr }.route_bundles[route_bundle_index.get() as usize]; + &raw mut self.route_bundles[route_bundle_index.get() as usize]; debug_assert!(matches!( // SAFETY: `route_bundle` points into `self.route_bundles`, not resized in this fn. unsafe { &(*route_bundle).data }, @@ -2834,10 +2827,10 @@ impl DevServer { let blob: *mut StaticRoute = match unsafe { (*route_bundle).data.html().cached_response } { Some(b) => b.as_ptr(), None => 'generate: { - // SAFETY: `generate_html_payload` reads `route_bundle.data` / - // `client_graph` and never reallocates `route_bundles`. No + // SAFETY: `route_bundle` points into `self.route_bundles`, which + // `generate_html_payload` neither reallocates nor mutates. No // `&mut` into `*route_bundle` is live across this call. - let payload = unsafe { &mut *self_ptr } + let payload = self .generate_html_payload(route_bundle_index, unsafe { &*route_bundle }) .expect("oom"); @@ -2845,8 +2838,7 @@ impl DevServer { crate::webcore::AnyBlob::from_owned_slice(payload), crate::server::static_route::InitFromBytesOptions { mime_type: Some(&MimeType::HTML), - // SAFETY: `self_ptr` is `&mut self` erased; live for this fn body. - server: unsafe { &*self_ptr }.server, + server: self.server, ..Default::default() }, ); @@ -3036,8 +3028,7 @@ impl DevServer { // Note: erase `self` to a raw pointer so `route_bundle` borrow // doesn't conflict with `generate_client_bundle(&mut self, ..)`. let self_ptr = std::ptr::from_mut::(self); - // SAFETY: `self_ptr` accesses below touch disjoint fields of `*self`. - let route_bundle = unsafe { &mut *self_ptr }.route_bundle_ptr(bundle_index); + let route_bundle = self.route_bundle_ptr(bundle_index); let client_bundle: *mut StaticRoute = match route_bundle.client_bundle { Some(cb) => cb.as_ptr(), None => 'generate: { @@ -5235,7 +5226,8 @@ fn on_request(dev: &mut DevServer, req: &mut Request, mut resp: AnyResponse) { .get_or_put_route_bundle(route_bundle::UnresolvedIndex::Framework(route_index)) .expect("oom"); let mut ctx = RequestEnsureRouteBundledCtx { - dev: std::ptr::from_mut::(dev), + // SAFETY: `dev` outlives the stack-local ctx; write provenance kept. + dev: unsafe { bun_ptr::ParentRef::from_raw_mut(std::ptr::from_mut::(dev)) }, req: ReqOrSaved::Req(req), resp, kind: deferred_request::HandlerKind::ServerHandler, @@ -5285,7 +5277,10 @@ impl DevServer { .get_or_put_route_bundle(route_bundle::UnresolvedIndex::Framework(route_index)) .expect("oom"); let mut ctx = RequestEnsureRouteBundledCtx { - dev: std::ptr::from_mut::(self), + // SAFETY: `self` outlives the stack-local ctx; write provenance kept. + dev: unsafe { + bun_ptr::ParentRef::from_raw_mut(std::ptr::from_mut::(self)) + }, req: ReqOrSaved::Saved(saved_request), resp, kind: deferred_request::HandlerKind::ServerHandler, @@ -5322,7 +5317,8 @@ impl DevServer { .get_or_put_route_bundle(route_bundle::UnresolvedIndex::Html(html)) .map_err(|_| AllocError)?; let mut ctx = RequestEnsureRouteBundledCtx { - dev: std::ptr::from_mut::(self), + // SAFETY: `self` outlives the stack-local ctx; write provenance kept. + dev: unsafe { bun_ptr::ParentRef::from_raw_mut(std::ptr::from_mut::(self)) }, req: ReqOrSaved::Req(req), resp, kind: deferred_request::HandlerKind::BundledHtmlPage, @@ -5531,11 +5527,7 @@ impl DevServer { if headers.get(b"etag").is_none() && !any_blob.slice().is_empty() { bun_http::headers::append_etag(any_blob.slice(), &mut headers); } - let fetch_headers = bun_http_jsc::headers_jsc::to_fetch_headers(&headers, global)?; - // SAFETY: `to_fetch_headers` returns a fresh +1 `FetchHeaders*`; - // ownership is transferred to `HeadersRef`. - let headers_ref = - unsafe { crate::webcore::response::HeadersRef::adopt(fetch_headers) }; + let headers_ref = bun_http_jsc::headers_jsc::to_fetch_headers(&headers, global)?; let response: Response = Response::init( crate::webcore::response::Init { status_code: 500, @@ -6079,16 +6071,17 @@ impl DevServer { // SAFETY: see above; `kinds` is a disjoint SoA column owned by `watchlist`. let kinds = unsafe { &*kinds }; - let ev_ptr = self.watcher_atomics.watcher_acquire_event(); - // SAFETY: `watcher_acquire_event` returns a valid `*mut HotReloadEvent` - // into `self.watcher_atomics.events`; exclusive on the watcher thread. + let ev_index = self.watcher_atomics.watcher_acquire_event(); + let ev_ptr: *mut HotReloadEvent = &raw mut self.watcher_atomics.events[ev_index as usize]; + // SAFETY: the watcher thread exclusively owns `events[ev_index]` until the + // deferred `watcher_release_and_submit_event` below. let ev = unsafe { &mut *ev_ptr }; // Note: erase `self` to a raw ptr in the deferred closures so the // loop body can keep using `self.bun_watcher`. let self_ptr: *mut Self = self; scopeguard::defer! { // SAFETY: `self_ptr` is live for the entire fn body; guard runs at scope exit. - unsafe { (*self_ptr).watcher_atomics.watcher_release_and_submit_event(ev_ptr) } + unsafe { (*self_ptr).watcher_atomics.watcher_release_and_submit_event(ev_index) } }; // SAFETY: see `self_ptr` SAFETY above. @@ -6612,9 +6605,9 @@ impl DevServer { /// Problem statement documented on `SCRIPT_UNREF_PAYLOAD` /// Takes 8 bytes: The generation ID in hex. struct UnrefSourceMapRequest { - // BACKREF: DevServer outlives the request; raw ptr avoids the `'static` - // bound on `BodyReaderHandler` that a borrowed `&mut DevServer` would violate. - dev: *mut DevServer, + // BACKREF: DevServer outlives the request; a non-owning `ParentRef` avoids the + // `'static` bound on `BodyReaderHandler` that `&mut DevServer` would violate. + dev: bun_ptr::ParentRef, body: uws::BodyReaderMixin, } @@ -6646,7 +6639,8 @@ impl UnrefSourceMapRequest { .expect("server bound") .on_pending_request(); let ctx = Box::new(UnrefSourceMapRequest { - dev: std::ptr::from_mut::(dev), + // SAFETY: DevServer outlives the request; write provenance kept. + dev: unsafe { bun_ptr::ParentRef::from_raw_mut(std::ptr::from_mut::(dev)) }, body: uws::BodyReaderMixin::init(), }); let raw = bun_core::heap::into_raw(ctx); @@ -6659,14 +6653,12 @@ impl UnrefSourceMapRequest { // SAFETY: caller contract — ctx is the original Box allocation; no // live borrow of *ctx exists. let ctx = unsafe { bun_core::heap::take(ctx) }; - // SAFETY: dev outlives the request - unsafe { - (*ctx.dev) - .server - .as_mut() - .unwrap() - .on_static_request_complete() - }; + // SAFETY: DevServer outlives the request; no other borrow of it is live. + unsafe { ctx.dev.assume_mut() } + .server + .as_mut() + .unwrap() + .on_static_request_complete(); drop(ctx); } @@ -6686,8 +6678,8 @@ impl UnrefSourceMapRequest { .map_err(|_| bun_core::err!(InvalidRequest))?; let generation = u32::from_ne_bytes(generation_bytes); let source_map_key = source_map_store::Key::init((generation as u64) << 32); - // SAFETY: ctx is live (caller contract); dev outlives the request. - let _ = unsafe { &mut *(*ctx).dev } + // SAFETY: `ctx` is live (caller contract); DevServer outlives the request. + let _ = unsafe { (*ctx).dev.assume_mut() } .source_maps .remove_or_upgrade_weak_ref( source_map_key, @@ -6739,9 +6731,9 @@ pub(crate) fn get_deinit_count_for_testing() -> usize { } struct PromiseEnsureRouteBundledCtx<'a> { - // Note: raw ptr — `dev` is re-borrowed across the ctx while also passed + // Non-owning backref: `dev` is re-borrowed across the ctx while also passed // as `&mut` into `ensure_route_is_bundled`. - dev: *mut DevServer, + dev: bun_ptr::ParentRef, global: &'a JSGlobalObject, promise: Option, p: Option<*mut jsc::JSPromise>, // BORROW_FIELD: from sibling self.promise @@ -6749,14 +6741,14 @@ struct PromiseEnsureRouteBundledCtx<'a> { } impl<'a> PromiseEnsureRouteBundledCtx<'a> { - /// Reborrow the erased `dev` pointer. - /// SAFETY: `self.dev` is set from a live `&mut DevServer` at ctx - /// construction; the ctx is stack-local in the request handler scope. + /// Exclusive borrow of the parent DevServer. + /// SAFETY: no other borrow of the DevServer overlaps the returned one; the + /// ctx is stack-local in the request handler scope. #[inline] fn dev_mut(&mut self) -> &mut DevServer { - // SAFETY: `self.dev` was set from a live `&mut DevServer` at ctx - // construction and outlives this stack-local ctx. - unsafe { &mut *self.dev } + // SAFETY: `dev` was built by `from_raw_mut` from a live `&mut DevServer` + // that outlives this stack-local ctx. + unsafe { self.dev.assume_mut() } } /// Reborrow the GC-heap `JSPromise` recorded in `self.p`. Single `unsafe` @@ -6969,12 +6961,11 @@ fn bundle_new_route_js_function_impl( }; // SAFETY: JS-thread single-writer; `dev_server_mut` returns the // `Box` slot in `NewServer` populated by `set_routes`. - let dev: &mut DevServer = unsafe { &mut *dev_ptr }; + let dev: &mut DevServer = unsafe { dev_ptr.assume_mut() }; let route_bundle_index = dev .get_or_put_route_bundle(route_bundle::UnresolvedIndex::Framework(route_index)) .expect("oom"); - let dev_ptr: *mut DevServer = dev; let mut ctx = PromiseEnsureRouteBundledCtx { dev: dev_ptr, global, @@ -6984,9 +6975,9 @@ fn bundle_new_route_js_function_impl( }; let rbi = ctx.route_bundle_index; - // SAFETY: `ctx.dev` aliases the same DevServer. Reborrow via raw ptr to - // satisfy borrowck while ctx is also &mut-borrowed. - ensure_route_is_bundled(unsafe { &mut *dev_ptr }, rbi, &mut ctx)?; + // SAFETY: `ctx.dev` aliases the same DevServer. Re-derive from the backref + // to satisfy borrowck while ctx is also &mut-borrowed. + ensure_route_is_bundled(unsafe { dev_ptr.assume_mut() }, rbi, &mut ctx)?; let array = JSValue::create_empty_array(global, 2)?; @@ -7083,7 +7074,7 @@ fn new_route_params_for_bundle_promise_for_js( }; // SAFETY: JS-thread single-writer; `dev_server_mut` returns the // `Box` slot in `NewServer` populated by `set_routes`. - let dev: &mut DevServer = unsafe { &mut *dev_ptr }; + let dev: &mut DevServer = unsafe { dev_ptr.assume_mut() }; let route_bundle_index = route_bundle::Index::init( u32::try_from(route_bundle_index_js.to_int32()).expect("int cast"), diff --git a/src/runtime/bake/FrameworkRouter.rs b/src/runtime/bake/FrameworkRouter.rs index 341ab015e3df..b0e103b7e9e9 100644 --- a/src/runtime/bake/FrameworkRouter.rs +++ b/src/runtime/bake/FrameworkRouter.rs @@ -1531,9 +1531,6 @@ impl FrameworkRouter { // program lifetime. Resolver mutex serializes mutation. We hold a raw pointer (no borrow) // so `r.read_dir_info_ignore_error(&mut self)` below does not conflict. let fs_ref = unsafe { &*fs }; - // SAFETY: `fs` is the non-null process-global FileSystem singleton (see above); - // `addr_of_mut!` only computes a field address without forming a reference. - let fs_impl = unsafe { core::ptr::addr_of_mut!((*fs).fs) }; { // Note: `entries.data` is backed by `std::collections::HashMap`, @@ -1569,10 +1566,7 @@ impl FrameworkRouter { let file = unsafe { &*file_ptr }; let base = file.base(); // Note: reshaped for borrowck — fetch type fields fresh each iteration. - // SAFETY: `Entry::kind` mutates only the entry's lazily-cached kind; `file_ptr` - // is the unique live reference to this entry during the scan, and `fs_impl` - // points at the process-global FS implementation. - match unsafe { (*file_ptr).kind(&raw mut *fs_impl, false) } { + match file.kind(bun_ptr::ParentRef::new(&fs_ref.fs), false) { bun_resolver::fs::EntryKind::Dir => { let t = &self.types[t_index.get() as usize]; if t.ignore_underscores && base.starts_with(b"_") { @@ -1586,7 +1580,7 @@ impl FrameworkRouter { } if let Some(child_info) = - r.read_dir_info_ignore_error(fs_ref.abs(&[file.dir, file.base()])) + r.read_dir_info_ignore_error(fs_ref.abs(&[file.dir(), file.base()])) { self.scan_inner(t_index, r, &child_info, arena_state, ctx)?; } @@ -1618,7 +1612,7 @@ impl FrameworkRouter { >( &mut rel_path_buf[1..], &self.root, - fs_ref.abs(&[file.dir, file.base()]), + fs_ref.abs(&[file.dir(), file.base()]), ); full_rel_path.len() }; @@ -1706,7 +1700,7 @@ impl FrameworkRouter { t_index, InsertPattern::Dynamic(pattern), file_kind, - fs_ref.abs(&[file.dir, file.base()]), + fs_ref.abs(&[file.dir(), file.base()]), ctx, &mut out_colliding_file_id, ) @@ -1737,7 +1731,7 @@ impl FrameworkRouter { t_index, InsertPattern::Static(pattern), file_kind, - fs_ref.abs(&[file.dir, file.base()]), + fs_ref.abs(&[file.dir(), file.base()]), ctx, &mut out_colliding_file_id, ) diff --git a/src/runtime/bake/dev_server/mod.rs b/src/runtime/bake/dev_server/mod.rs index e460537de24e..f8cf79d7b3d7 100644 --- a/src/runtime/bake/dev_server/mod.rs +++ b/src/runtime/bake/dev_server/mod.rs @@ -757,7 +757,7 @@ pub struct WatcherAtomics { pub pending_event: Option, // Debug fields to ensure methods are being called in the right order. #[cfg(debug_assertions)] - pub dbg_watcher_event: Option<*mut HotReloadEvent>, + pub dbg_watcher_event: Option, #[cfg(debug_assertions)] pub dbg_server_event: Option<*mut HotReloadEvent>, } @@ -843,12 +843,12 @@ impl WatcherAtomics { Some(event) } - /// Atomically get a `*mut HotReloadEvent` that is not in use by the - /// DevServer thread. Call `watcher_release_and_submit_event` when it is - /// filled with files. + /// Atomically get the index of a `HotReloadEvent` that is not in use by + /// the DevServer thread. Call `watcher_release_and_submit_event` when it + /// is filled with files. /// /// Called from watcher thread. - pub fn watcher_acquire_event(&mut self) -> *mut HotReloadEvent { + pub fn watcher_acquire_event(&mut self) -> u8 { let mut available = [true; 3]; if let Some(i) = self.current_event { available[i as usize] = false; @@ -865,7 +865,7 @@ impl WatcherAtomics { } unreachable!() }; - let ev: *mut HotReloadEvent = &raw mut self.events[index]; + let index = u8::try_from(index).unwrap(); #[cfg(debug_assertions)] { @@ -873,12 +873,12 @@ impl WatcherAtomics { self.dbg_watcher_event.is_none(), "must call `watcherReleaseEvent` before calling `watcherAcquireEvent` again", ); - self.dbg_watcher_event = Some(ev); + self.dbg_watcher_event = Some(index); } - // SAFETY: `ev` points into `self.events[index]`, which the watcher thread has exclusive - // access to (it is neither `current_event` nor `pending_event`). - let ev_ref = unsafe { &mut *ev }; + // The watcher thread has exclusive access to `events[index]` + // (it is neither `current_event` nor `pending_event`). + let ev_ref = &mut self.events[index as usize]; // Initialize the timer if it is empty. if ev_ref.is_empty() { @@ -891,54 +891,43 @@ impl WatcherAtomics { #[cfg(debug_assertions)] debug_assert!(ev_ref.debug_mutex.try_lock()); - ev + index } - /// Release the pointer from `watcher_acquire_event`, submitting the event + /// Release the index from `watcher_acquire_event`, submitting the event /// if it contains new files. /// /// Called from watcher thread. - /// - /// # Safety - /// `ev` must be the pointer returned by the matching - /// `watcher_acquire_event` call (a slot in `self.events`), and the watcher - /// thread must still hold exclusive access to it. // `&(...)` is deliberate — sidesteps dangerous_implicit_autorefs. #[allow(clippy::needless_borrow)] - pub(crate) fn watcher_release_and_submit_event(&mut self, ev: *mut HotReloadEvent) { - // SAFETY: per this function's contract. - let ev_ref = unsafe { &mut *ev }; - - ev_ref.assert_watcher_thread_locked(); + pub(crate) fn watcher_release_and_submit_event(&mut self, ev_index: u8) { + self.events[ev_index as usize].assert_watcher_thread_locked(); #[cfg(debug_assertions)] { - let Some(dbg_event) = self.dbg_watcher_event else { + let Some(dbg_index) = self.dbg_watcher_event else { panic!("must call `watcherAcquireEvent` before `watcherReleaseAndSubmitEvent`"); }; debug_assert!( - dbg_event == ev, + dbg_index == ev_index, "watcherReleaseAndSubmitEvent: event is not from last `watcherAcquireEvent` call \ - (expected {:p}, got {:p})", - dbg_event, - ev, + (expected {}, got {})", + dbg_index, + ev_index, ); self.dbg_watcher_event = None; } #[cfg(debug_assertions)] { - ev_ref.debug_mutex.unlock(); + self.events[ev_index as usize].debug_mutex.unlock(); } - if ev_ref.is_empty() { + if self.events[ev_index as usize].is_empty() { return; } // There are files to be processed. - // SAFETY: `ev` points into `self.events`; both are within the same allocation. - let ev_index: u8 = - u8::try_from(unsafe { ev.offset_from(self.events.as_ptr().cast_mut()) }).unwrap(); let old_next = NextEvent(self.next_event.swap(ev_index, Ordering::AcqRel)); match old_next { NextEvent::DONE => { @@ -956,8 +945,14 @@ impl WatcherAtomics { "no event should be running right now", ); // Not atomic because the dev server is not running events right now. - self.dbg_server_event = Some(ev); + self.dbg_server_event = Some(&raw mut self.events[ev_index as usize]); } + // `ev` must be the parent of `ev_ref`: the pointer stored in the + // task is written through below, and a child write would disable a + // sibling raw. + let ev: *mut HotReloadEvent = &raw mut self.events[ev_index as usize]; + // SAFETY: the watcher thread still exclusively owns this slot. + let ev_ref = unsafe { &mut *ev }; ev_ref.concurrent_task = bun_event_loop::ConcurrentTask::ConcurrentTask { task: bun_event_loop::Task::init(ev), ..Default::default() diff --git a/src/runtime/bake/dev_server/source_map_store.rs b/src/runtime/bake/dev_server/source_map_store.rs index 9c3372ca523e..3e8e8a94f8b0 100644 --- a/src/runtime/bake/dev_server/source_map_store.rs +++ b/src/runtime/bake/dev_server/source_map_store.rs @@ -630,59 +630,68 @@ impl SourceMapStore { } /// `SourceMapStore.sweepWeakRefs` — pop expired weak-refs, decrement, - /// reschedule. Called from the high-tier `EventLoopTimer` dispatch with - /// the raw `*EventLoopTimer`. + /// reschedule. Trampoline for the high-tier `EventLoopTimer` dispatch: + /// recovers the owning `DevServer` once, then runs the safe body. /// /// # Safety /// `timer` must point to the `weak_ref_sweep_timer` field of a live /// `SourceMapStore` that is itself the `source_maps` field of a live /// heap-allocated `DevServer`. - // `timer` is never dereferenced in Rust — `from_timer_ptr` only does - // `container_of` pointer arithmetic to recover the parent `SourceMapStore`; - // the deref is of that recovered parent pointer, not the parameter. - // not_unsafe_ptr_arg_deref is a false positive on this fieldParentPtr pattern. #[allow(clippy::not_unsafe_ptr_arg_deref)] pub fn sweep_weak_refs( timer: *mut EventLoopTimer, now_ts: &bun_event_loop::EventLoopTimer::Timespec, + ) { + // SAFETY: caller contract — two `container_of` steps recover the live + // `DevServer`; no other reference to it is live for this call. + let dev: &mut DevServer = unsafe { + let store = SourceMapStore::from_timer_ptr(timer); + &mut *bun_core::from_field_ptr!(DevServer, source_maps, store) + }; + Self::sweep_weak_refs_inner(dev, now_ts); + } + + /// Safe body of the sweep. Borrows the whole `DevServer` so that no + /// `&mut SourceMapStore` is live across the (re-entrant) + /// `emit_memory_visualizer_message_if_needed`. + fn sweep_weak_refs_inner( + dev: &mut DevServer, + now_ts: &bun_event_loop::EventLoopTimer::Timespec, ) { map_log!("sweepWeakRefs"); - // SAFETY: `timer` points to the `weak_ref_sweep_timer` field of a SourceMapStore. - let store: &mut SourceMapStore = unsafe { &mut *SourceMapStore::from_timer_ptr(timer) }; - // SAFETY: invariant of `owner()` — store is the `source_maps` field of a live DevServer. - debug_assert!(unsafe { (*store.owner()).magic } == Magic::Valid); + debug_assert!(dev.magic == Magic::Valid); // Mixed-sign comparison: a negative `expire` must count as expired. // Keep `now` as i64 (already clamped ≥0) so the comparison stays // sign-correct without u64 wrap. let now: i64 = now_ts.sec.max(0); - // `emitMemoryVisualizerMessageIfNeeded` is inlined at both returns - // (a scopeguard cannot capture &mut store across the loop body - // without aliasing). + // `emitMemoryVisualizerMessageIfNeeded` is inlined at both returns, + // after every borrow of `dev.source_maps` has ended. - while let Some(item) = store.weak_refs.read_item() { + while let Some(item) = dev.source_maps.weak_refs.read_item() { if item.expire <= now { - store.unref_count(item.key(), item.count); + dev.source_maps.unref_count(item.key(), item.count); } else { - store.weak_refs.unget(&[item]).expect("unreachable"); // space exists since the last item was just removed. - store.weak_ref_sweep_timer.state = EventLoopTimerState::FIRED; + dev.source_maps + .weak_refs + .unget(&[item]) + .expect("unreachable"); // space exists since the last item was just removed. + dev.source_maps.weak_ref_sweep_timer.state = EventLoopTimerState::FIRED; Self::timer_all().update( - core::ptr::addr_of_mut!(store.weak_ref_sweep_timer), + core::ptr::addr_of_mut!(dev.source_maps.weak_ref_sweep_timer), &Timespec { sec: item.expire + 1, nsec: 0, }, ); - // SAFETY: invariant of `owner()`. - unsafe { (*store.owner()).emit_memory_visualizer_message_if_needed() }; + dev.emit_memory_visualizer_message_if_needed(); return; } } - store.weak_ref_sweep_timer.state = EventLoopTimerState::CANCELLED; - // SAFETY: invariant of `owner()`. - unsafe { (*store.owner()).emit_memory_visualizer_message_if_needed() }; + dev.source_maps.weak_ref_sweep_timer.state = EventLoopTimerState::CANCELLED; + dev.emit_memory_visualizer_message_if_needed(); } /// This is used in exactly one place: remapping errors. diff --git a/src/runtime/bake/production.rs b/src/runtime/bake/production.rs index f61de35ad562..705f6595ccc1 100644 --- a/src/runtime/bake/production.rs +++ b/src/runtime/bake/production.rs @@ -110,14 +110,11 @@ pub fn build_command(ctx: Context) -> Result<(), bun_core::Error> { smol: ctx.runtime_options.smol, ..Default::default() })?; - // SAFETY: `init_bake` returns a freshly-allocated VM owned by this thread; - // unique access for the rest of this function. - let vm = unsafe { &mut *vm_ptr }; + // `init_bake` installs the VM as this thread's singleton. + let vm = VirtualMachine::get_mut(); // defer vm.deinit() — handled by `vm.destroy()` on the unwind path below. // Note: pass `vm_ptr` by value into the guard so the drop closure does - // not borrow the local (`defer!` would capture `&vm_ptr`, which under - // edition-2024 disjoint-capture rules collides with the `&mut *vm_ptr` - // re-borrows on the JSError path). + // not borrow the local. let _vm_guard = scopeguard::guard(vm_ptr, |p| { // SAFETY: p is the unique live VM on this thread. unsafe { (*p).destroy() }; @@ -199,46 +196,38 @@ pub fn build_command(ctx: Context) -> Result<(), bun_core::Error> { } // `vm.log` was set from `ctx.log` above (non-null, process-lifetime); // `log_mut()` is the safe accessor encapsulating the NonNull deref. + let vm = VirtualMachine::get(); bun_http::async_http::load_env(vm.log_mut().unwrap(), vm.env_loader()); - vm.load_extra_env_and_source_code_printer(); - vm.is_main_thread = true; + VirtualMachine::get_mut().load_extra_env_and_source_code_printer(); + VirtualMachine::get_mut().is_main_thread = true; jsc::virtual_machine::IS_MAIN_THREAD_VM.set(true); - // SAFETY: vm.jsc_vm is the live JSC::VM* set in `VirtualMachine::initBake`; - // raw-ptr deref yields an unbounded `&VM` so the `ApiLock<'_>` does not - // borrow `vm` (the VirtualMachine) and the body below can keep using it. + // `jsc_vm()` hands out a `&'static VM` (a separate JSC allocation), so the + // lock does not borrow the VirtualMachine. // // Declaration order matters: `_api_lock` is bound before `pt` so LIFO drop // detaches `pt` (a JSC FFI call) *while the API lock is still held*, then // releases the lock. - let _api_lock = unsafe { (*vm.jsc_vm).get_api_lock() }; + let _api_lock = VirtualMachine::get().jsc_vm().get_api_lock(); // Note: `PerThread` owns its data. Start with an empty placeholder so Drop // (which detaches the C++-side per-thread pointer) runs in this frame's // LIFO order — under the API lock, before the VM is destroyed. let mut pt = PerThread::placeholder(vm_ptr); - // Note: reshaped for borrowck — `pt.vm` already borrows `*vm`, so pass - // the raw VM pointer and re-borrow inside. - match build_with_vm(ctx, &cwd, vm_ptr, &mut pt) { + match build_with_vm(ctx, &cwd, &mut pt) { Ok(()) => {} Err(e) if e == bun_core::err!("JSError") => { bun_crash_handler::handle_error_return_trace(e, None); - // SAFETY: vm.global is live for VM lifetime. - let global = unsafe { &*(*vm_ptr).global }; + let global = VirtualMachine::get().global(); let err_value = global.take_exception(jsc::JsError::Thrown); - // SAFETY: see above. - unsafe { - (*vm_ptr) - .print_error_like_object_to_console(err_value.to_error().unwrap_or(err_value)) - }; - // SAFETY: see above. - let vm = unsafe { &mut *vm_ptr }; - if vm.exit_handler.exit_code == 0 { - vm.exit_handler.exit_code = 1; + VirtualMachine::get_mut() + .print_error_like_object_to_console(err_value.to_error().unwrap_or(err_value)); + if VirtualMachine::get().exit_handler.exit_code == 0 { + VirtualMachine::get_mut().exit_handler.exit_code = 1; } - vm.on_exit(); - vm.global_exit(); + VirtualMachine::get_mut().on_exit(); + VirtualMachine::get_mut().global_exit(); } Err(e) => return Err(e), } @@ -288,15 +277,11 @@ pub(super) fn write_sourcemap_to_disk( pub(super) fn build_with_vm( ctx: Context, cwd: &[u8], - vm_ptr: *mut VirtualMachine, pt: &mut PerThread, ) -> Result<(), bun_core::Error> { - // SAFETY: vm_ptr is the live per-thread VM passed from build_command; - // exclusive access on this thread for the duration of the call. - let vm = unsafe { &mut *vm_ptr }; // Load and evaluate the configuration module. `global()` returns - // `&'static`, decoupled from `vm` so later `&mut vm` reborrows are allowed. - let global = vm.global(); + // `&'static`, decoupled from the VM so every `&mut` reborrow stays short. + let global = VirtualMachine::get().global(); // allocator = bun.default_allocator — dropped per §Allocators bun_core::pretty_errorln!("Loading configuration"); @@ -313,7 +298,7 @@ pub(super) fn build_with_vm( unresolved_config_entry_point = prefixed; } - let config_entry_point = match vm.transpiler.resolver.resolve( + let config_entry_point = match VirtualMachine::get_mut().transpiler.resolver.resolve( cwd, &unresolved_config_entry_point, bun_ast::ImportKind::EntryPointBuild, @@ -348,9 +333,10 @@ pub(super) fn build_with_vm( let config_entry_point_string = BunString::clone_utf8(config_entry_point.path_const().unwrap().text); - let Some(config_promise) = - JSModuleLoader::load_and_evaluate_module_ptr(vm.global, Some(&config_entry_point_string)) - else { + let Some(config_promise) = JSModuleLoader::load_and_evaluate_module_ptr( + VirtualMachine::get().global, + Some(&config_entry_point_string), + ) else { debug_assert!(global.has_exception()); return Err(bun_core::err!("JSError")); }; @@ -359,8 +345,8 @@ pub(super) fn build_with_vm( // `opaque_mut` is the const-asserted safe `*mut → &mut` accessor // (`load_and_evaluate_module_ptr` returned a live JSC-heap cell). jsc::JSInternalPromise::opaque_mut(config_promise_ptr).set_handled(); - vm.wait_for_promise(AnyPromise::Internal(config_promise_ptr)); - let jsc_vm = vm.jsc_vm_mut(); + VirtualMachine::get_mut().wait_for_promise(AnyPromise::Internal(config_promise_ptr)); + let jsc_vm = VirtualMachine::get_mut().jsc_vm_mut(); // Promise cell is still live (rooted via the module loader). let mut options = match jsc::JSInternalPromise::opaque_mut(config_promise_ptr) .unwrap(jsc_vm, UnwrapMode::MarkHandled) @@ -445,7 +431,7 @@ pub(super) fn build_with_vm( let mut ssr_transpiler = MaybeUninit::::uninit(); // `vm.log` is set from `ctx.log` (non-null, process-lifetime); // `log_mut()` is the safe accessor encapsulating the NonNull deref. - let vm_log = vm.log_mut().unwrap(); + let vm_log = VirtualMachine::get().log_mut().unwrap(); framework.init_transpiler_with_options( &options.arena, vm_log, @@ -636,7 +622,8 @@ pub(super) fn build_with_vm( // Construct the `AnyEventLoop` enum // value (NOT a pointer-cast: the bundler matches on its discriminant). // Lives in this block's stack frame, outliving the bundle call. - let mut any_loop = bun_event_loop::AnyEventLoop::js(vm.event_loop().cast()); + let mut any_loop = + bun_event_loop::AnyEventLoop::js(VirtualMachine::get().event_loop().cast()); // Propagate via `?`. Do NOT // catch-and-exit here: the bake path expects this call to succeed for @@ -828,7 +815,7 @@ pub(super) fn build_with_vm( } *pt = PerThread::init( - vm_ptr, + VirtualMachine::get_mut_ptr(), entry_points, bundled_outputs_list, module_keys, @@ -1218,12 +1205,8 @@ pub(super) fn build_with_vm( ) }; render_promise.set_handled(); - // Rebind from the raw pointer: `PerThread::init`/`attach`/`load_bundled_module` - // above accessed the same allocation through `vm_ptr`, invalidating the - // earlier `&mut` under Stacked Borrows. - let vm = VirtualMachine::get().as_mut(); - vm.wait_for_promise(AnyPromise::Normal(render_promise)); - let jsc_vm = vm.jsc_vm_mut(); + VirtualMachine::get_mut().wait_for_promise(AnyPromise::Normal(render_promise)); + let jsc_vm = VirtualMachine::get_mut().jsc_vm_mut(); match render_promise.unwrap(jsc_vm, UnwrapMode::MarkHandled) { Unwrapped::Pending => unreachable!(), Unwrapped::Fulfilled(_) => { @@ -1234,7 +1217,7 @@ pub(super) fn build_with_vm( return Err(js_err(global.throw_value(err))); } } - vm.wait_for_tasks(); + VirtualMachine::get_mut().wait_for_tasks(); Ok(()) } diff --git a/src/runtime/cli/build_command.rs b/src/runtime/cli/build_command.rs index 1a09385020f6..8656200bf7d4 100644 --- a/src/runtime/cli/build_command.rs +++ b/src/runtime/cli/build_command.rs @@ -65,7 +65,8 @@ impl BuildCommand { let log = ctx.log; // SAFETY: `ctx.log` is a long-lived `*mut Log` set up during CLI init // and never freed for the duration of the command body. - let log_ref: &mut bun_ast::Log = unsafe { &mut *log }; + let log_ref: bun_ptr::ParentRef = + unsafe { bun_ptr::ParentRef::from_raw_mut(log) }; let user_requested_browser_target = ctx.args.target.is_some() && ctx.args.target.unwrap() == api::Target::Browser; if ctx.bundler_options.compile || ctx.bundler_options.bytecode { @@ -519,8 +520,11 @@ impl BuildCommand { raw.insert(key.as_ref(), value.clone()); } let drop: Vec<&[u8]> = ctx.args.drop.iter().map(|d| d.as_ref()).collect(); + // SAFETY: no other borrow of the CLI `Log` is live here, and + // `from_input` never reaches JS. + let log = unsafe { log_ref.assume_mut() }; Some(bun_bundler::defines::DefineData::from_input( - &raw, &drop, log_ref, arena, + &raw, &drop, log, arena, )?) } None => None, @@ -576,7 +580,9 @@ impl BuildCommand { let opt_output_format = this_transpiler.options.output_format; let opt_source_map = this_transpiler.options.source_map; let opt_transform_only = this_transpiler.options.transform_only; - let env_ptr = this_transpiler.env; + // SAFETY: `Transpiler::init` leaves `env` non-null (loader singleton); + // `from_raw_mut` keeps the write provenance `assume_mut` needs below. + let env_ptr = unsafe { bun_ptr::ParentRef::from_raw_mut(this_transpiler.env) }; let mut output_files: Vec = 'brk: { if ctx.bundler_options.transform_only { @@ -867,8 +873,9 @@ impl BuildCommand { root_dir.fd, &opt_public_path, outfile, - // SAFETY: `env` is a process-lifetime singleton. - unsafe { &mut *env_ptr }, + // SAFETY: no other borrow of the env loader is live for this + // call, and `to_executable` never reaches JS. + unsafe { env_ptr.assume_mut() }, opt_output_format, &ctx.bundler_options.windows, ctx.bundler_options diff --git a/src/runtime/cli/bunx_command.rs b/src/runtime/cli/bunx_command.rs index 28016e39df98..86d7cd317706 100644 --- a/src/runtime/cli/bunx_command.rs +++ b/src/runtime/cli/bunx_command.rs @@ -891,8 +891,7 @@ impl BunxCommand { }; env_loader.map.put(b"PATH", &path)?; - // SAFETY: `Transpiler::init` always sets `fs` to the process singleton. - let fs = unsafe { &mut *this_transpiler.fs }; + let fs = this_transpiler.fs_mut(); let uid_digits = bun_core::fmt::digit_count(uid); let bunx_cache_dir: &[u8] = &path[0..temp_dir.len() + b"/bunx--".len() + package_fmt.len() + uid_digits]; @@ -1296,18 +1295,9 @@ impl BunxCommand { bun_event_loop::MiniEventLoop::init_global( // `this_transpiler.env` is the process-lifetime loader // singleton populated during transpiler init. - // - // Aliasing: do NOT call `this_transpiler.env_mut()` here — - // `env_loader` (line 594) is still live and is used again below at the - // post-install `Run::run_binary` calls. A second `env_mut()` would - // `unsafe { &mut *self.env }` from the raw field, popping `env_loader`'s - // Unique tag under Stacked Borrows (UB on later use). Instead reborrow - // *through* `env_loader` so the new `&mut` is a child of its tag; the - // child is consumed by `init_global` (converted to `NonNull`) before - // `env_loader` is touched again. - // SAFETY: `env_loader` is a valid `&'static mut Loader`; this is a - // stacked reborrow, not a sibling alias. - Some(unsafe { &mut *(env_loader as *mut _) }), + // SAFETY: the loader outlives the process; the raw cast keeps + // write provenance and creates no `&mut` aliasing `env_loader`. + Some(unsafe { bun_ptr::ParentRef::from_raw_mut(env_loader as *mut _) }), None, ), ), diff --git a/src/runtime/cli/create_command.rs b/src/runtime/cli/create_command.rs index d32f2fc2f56f..a53cbd00d3d5 100644 --- a/src/runtime/cli/create_command.rs +++ b/src/runtime/cli/create_command.rs @@ -32,11 +32,6 @@ use crate::cli::which_npm_client::NPMClient; #[path = "create/SourceFileProjectGenerator.rs"] pub mod SourceFileProjectGenerator; -// PORTING.md §Global mutable state: single-thread CLI scratch buffer → -// RacyCell. Touched on the main thread for `--open` *and* the spawned git -// thread (sequenced — git thread writes after main is done with it). -static BUN_PATH_BUF: bun_core::RacyCell = bun_core::RacyCell::new(PathBuffer::ZEROED); - // bun.OSPathLiteral — `bun_paths` does not (yet) export an // `os_path_literal!` macro from this crate's POV. `OSPathSlice` is `[u8]` on // POSIX, so byte-string literals coerce directly; the Windows `[u16]` form will @@ -277,8 +272,8 @@ impl CreateCommand { return CreateListExamplesCommand::exec(ctx); } - // SAFETY: `fs::FileSystem::init` returns a process-global singleton pointer. - let filesystem: &mut fs::FileSystem = unsafe { &mut *fs::FileSystem::init(None)? }; + fs::FileSystem::init(None)?; + let filesystem: &mut fs::FileSystem = fs::FileSystem::instance(); let mut env_loader: DotEnv::Loader = { DotEnv::Loader::init(crate::cli::cli_arena().alloc(DotEnv::Map::init())) }; @@ -1603,9 +1598,8 @@ impl CreateCommand { Output::flush(); if create_options.open { - // SAFETY: single-threaded CLI access to module-level static path buffer - let bun_path_buf = unsafe { &mut *BUN_PATH_BUF.get() }; - if let Some(bin) = which(bun_path_buf, path_env, destination, b"bun") { + let mut bun_path_buf = PathBuffer::uninit(); + if let Some(bin) = which(&mut bun_path_buf, path_env, destination, b"bun") { let argv: [&[u8]; 1] = [bin.as_bytes()]; crate::cli::open::open_url(bun_core::zstr!("http://localhost:3000/")); @@ -2083,7 +2077,6 @@ impl ExampleTag { // RacyCell. `URL_` borrows into the `*_BUF` statics so they must remain // process-lifetime, not stack locals. static URL_: bun_core::RacyCell>> = bun_core::RacyCell::new(None); -static APP_NAME_BUF: bun_core::RacyCell<[u8; 512]> = bun_core::RacyCell::new([0u8; 512]); static GITHUB_REPOSITORY_URL_BUF: bun_core::RacyCell<[u8; 1024]> = bun_core::RacyCell::new([0u8; 1024]); // Static so the borrowed slice satisfies `URL<'static>` for @@ -2096,20 +2089,22 @@ impl Example { pub fn print(examples: &[Example], default_app_name: Option<&[u8]>) { for example in examples { - // SAFETY: single-threaded CLI access to static buffer - let app_name_buf = unsafe { &mut *APP_NAME_BUF.get() }; - let app_name: &[u8] = default_app_name.unwrap_or_else(|| { - let mut cursor: &mut [u8] = &mut app_name_buf[..]; - let cap = cursor.len(); - write!( - &mut cursor, - "./{}-app", - bstr::BStr::new(&example.name[0..example.name.len().min(492)]) - ) - .expect("unreachable"); - let written = cap - cursor.len(); - &app_name_buf[..written] - }); + let mut app_name_buf = [0u8; 512]; + let app_name: &[u8] = match default_app_name { + Some(name) => name, + None => { + let mut cursor: &mut [u8] = &mut app_name_buf[..]; + let cap = cursor.len(); + write!( + &mut cursor, + "./{}-app", + bstr::BStr::new(&example.name[0..example.name.len().min(492)]) + ) + .expect("unreachable"); + let written = cap - cursor.len(); + &app_name_buf[..written] + } + }; if !example.description.is_empty() { bun_core::pretty!( @@ -2141,8 +2136,7 @@ impl Example { let mut examples: Vec = remote_examples.into_vec(); { - // SAFETY: single-threaded CLI access to module-level static path buffer - let home_dir_buf = unsafe { &mut *HOME_DIR_BUF.get() }; + let mut home_dir_buf = PathBuffer::uninit(); let mut folders: [bun_sys::Dir; 3] = [ bun_sys::Dir::from_fd(bun_sys::Fd::invalid()), bun_sys::Dir::from_fd(bun_sys::Fd::invalid()), @@ -2150,21 +2144,21 @@ impl Example { ]; if let Some(home_dir) = env_loader.map.get(b"BUN_CREATE_DIR") { let parts = [home_dir]; - let outdir_path = filesystem.abs_buf(&parts, home_dir_buf); + let outdir_path = filesystem.abs_buf(&parts, &mut home_dir_buf); folders[0] = bun_sys::Dir::open(outdir_path) .unwrap_or_else(|_| bun_sys::Dir::from_fd(bun_sys::Fd::invalid())); } { let parts = [filesystem.top_level_dir, BUN_CREATE_DIR]; - let outdir_path = filesystem.abs_buf(&parts, home_dir_buf); + let outdir_path = filesystem.abs_buf(&parts, &mut home_dir_buf); folders[1] = bun_sys::Dir::open(outdir_path) .unwrap_or_else(|_| bun_sys::Dir::from_fd(bun_sys::Fd::invalid())); } if let Some(home_dir) = env_loader.map.get(bun_core::env_var::HOME.key()) { let parts = [home_dir, BUN_CREATE_DIR]; - let outdir_path = filesystem.abs_buf(&parts, home_dir_buf); + let outdir_path = filesystem.abs_buf(&parts, &mut home_dir_buf); folders[2] = bun_sys::Dir::open(outdir_path) .unwrap_or_else(|_| bun_sys::Dir::from_fd(bun_sys::Fd::invalid())); } @@ -2692,7 +2686,7 @@ pub(crate) struct CreateListExamplesCommand; impl CreateListExamplesCommand { pub(crate) fn exec(ctx: &Command::Context) -> Result<(), bun_core::Error> { - let filesystem = fs::FileSystem::init(None)?; + fs::FileSystem::init(None)?; let mut env_loader: DotEnv::Loader = { DotEnv::Loader::init(crate::cli::cli_arena().alloc(DotEnv::Map::init())) }; @@ -2707,8 +2701,7 @@ impl CreateListExamplesCommand { let node: *mut ProgressNode = progress.start(b"Fetching manifest", 0); progress.refresh(); - // SAFETY: FileSystem::init returns the process-global singleton; valid for 'static. - let filesystem = unsafe { &mut *filesystem }; + let filesystem = fs::FileSystem::instance(); // SAFETY: `node` points into `progress`, which outlives this call; single-threaded. let examples = Example::fetch_all_local_and_remote( ctx, @@ -2823,10 +2816,7 @@ impl GitHandler { // Time (mean ± σ): 306.7 ms ± 6.1 ms [User: 31.7 ms, System: 269.8 ms] // Range (min … max): 299.5 ms … 318.8 ms 10 runs - // SAFETY: single-threaded CLI access to module-level static path buffer (note: this fn - // may run on the git thread; BUN_PATH_BUF is also touched on main thread for `--open`. - // The two uses are sequenced — git runs before `--open` block.) - let bun_path_buf = unsafe { &mut *BUN_PATH_BUF.get() }; + let mut bun_path_buf = PathBuffer::uninit(); // `bun.spawnSync` on Windows drives `uv_spawn` and needs a uv loop. This fn // runs on the dedicated git thread (see `GitHandler::spawn`), so use the // *thread-local* `MiniEventLoop` singleton — `init_global` is `thread_local!`-backed, @@ -2835,7 +2825,7 @@ impl GitHandler { let win_loop = bun_event_loop::EventLoopHandle::init_mini( bun_event_loop::MiniEventLoop::init_global(None, None), ); - if let Some(git) = which(bun_path_buf, path, destination, b"git") { + if let Some(git) = which(&mut bun_path_buf, path, destination, b"git") { let git: &[u8] = git.as_bytes(); let git_commands: [&[&[u8]]; 3] = [ &[git, b"init", b"--quiet"], diff --git a/src/runtime/cli/exec_command.rs b/src/runtime/cli/exec_command.rs index 94b104e7c199..69058bab4e22 100644 --- a/src/runtime/cli/exec_command.rs +++ b/src/runtime/cli/exec_command.rs @@ -62,11 +62,9 @@ impl ExecCommand { Global::exit(1); } }; - // SAFETY: `Transpiler::init` always populates `env` (caller-supplied, - // process singleton, or freshly `heap::alloc`'d) — never null. The - // loader is a thread-/process-lifetime singleton, so `&'static mut` is - // sound for the single CLI dispatch thread. - let env = unsafe { &mut *bundle.env.cast::>() }; + // SAFETY: `bundle.env` is the process-lifetime loader singleton set in + // `Transpiler::init`; no `&mut` to it is live here. + let env = unsafe { bun_ptr::ParentRef::from_raw_mut(bundle.env.cast()) }; let mini = bun_event_loop::MiniEventLoop::init_global(Some(env), Some(cwd)); let parts: [&[u8]; 2] = [cwd, b"[eval]"]; let script_path = bun_paths::resolve_path::join::(&parts); diff --git a/src/runtime/cli/filter_run.rs b/src/runtime/cli/filter_run.rs index 6f771aa805d5..3a572ba11591 100644 --- a/src/runtime/cli/filter_run.rs +++ b/src/runtime/cli/filter_run.rs @@ -871,9 +871,9 @@ pub(crate) fn run_scripts_with_filter( // SAFETY: Transpiler::init always sets `env` to the process-lifetime singleton. let env_ptr: *mut bun_dotenv::Loader<'static> = this_transpiler.env; + // SAFETY: `env_ptr` is the process-lifetime loader singleton (see above). let event_loop = MiniEventLoopMod::init_global( - // SAFETY: see above; `&'static mut` reborrow of the singleton for first-init only. - Some(unsafe { &mut *env_ptr }), + Some(unsafe { bun_ptr::ParentRef::from_raw_mut(env_ptr) }), None, ); // --no-orphans: register the macOS kqueue parent watch on this MiniEventLoop diff --git a/src/runtime/cli/init_command.rs b/src/runtime/cli/init_command.rs index 2d121c847ef9..6ce1cb93d66d 100644 --- a/src/runtime/cli/init_command.rs +++ b/src/runtime/cli/init_command.rs @@ -639,10 +639,7 @@ impl InitCommand { true }; - // SAFETY: `fields.object` was set above either from the parsed JSON - // (arena-owned, lives for the duration of `exec`) or from a freshly - // allocated `Expr.init` from the AST store (also lives until process exit). - let object = unsafe { &mut *fields.object.unwrap().as_ptr() }; + let object = &mut **fields.object.as_mut().unwrap(); if !minimal { if !fields.name.is_empty() { @@ -1407,8 +1404,7 @@ impl Template { head: bun_ast::Expr::init(bun_ast::E::String::init(b"scripts"), bun_ast::Loc::EMPTY), next: core::ptr::null_mut(), }); - // SAFETY: object is arena-allocated and live for the command duration. - let object = unsafe { &mut *fields.object.unwrap().as_ptr() }; + let object = &mut **fields.object.as_mut().unwrap(); let mut scripts_json = object.get_or_put_object(key, bump)?; let the_scripts = self.scripts(); let mut i: usize = 0; diff --git a/src/runtime/cli/mod.rs b/src/runtime/cli/mod.rs index 42d86f9b64e1..0ba2e3433648 100644 --- a/src/runtime/cli/mod.rs +++ b/src/runtime/cli/mod.rs @@ -1210,6 +1210,10 @@ pub mod command { if let Some(graph) = bun_standalone_graph::Graph::from_executable()? { // Never taken for a plain `bun` binary; ~2 KB of argv-splice // and ctx-setup code lives behind this cold call. + // SAFETY: non-null interior pointer of the process-static + // `UnsafeCell`; `JsCell` is `repr(transparent)` over it. + let graph: &'static bun_jsc::JsCell = + unsafe { &*graph.cast::>() }; return boot_standalone(graph, log); } } @@ -1348,12 +1352,10 @@ pub mod command { #[cold] #[inline(never)] fn boot_standalone( - graph: *mut bun_standalone_graph::Graph, + graph: &'static bun_jsc::JsCell, log: &mut bun_ast::Log, ) -> CmdResult { - // SAFETY: `from_executable` returns a non-null `*mut Graph` whose - // backing storage is process-static (owned by the executable image). - let graph: &mut bun_standalone_graph::Graph = unsafe { &mut *graph }; + let compile_exec_argv = graph.get().compile_exec_argv; let offset_for_passthrough: usize; let ctx: &mut ContextData = 'brk: { @@ -1363,10 +1365,10 @@ pub mod command { // standalone executable silently drops `BUN_OPTIONS` flags. let original_argv_len = bun::argv().len(); let bun_options_argc = bun::bun_options_argc(); - if !graph.compile_exec_argv.is_empty() || bun_options_argc > 0 { + if !compile_exec_argv.is_empty() || bun_options_argc > 0 { let mut argv_list: Vec<&'static bun_core::ZStr> = bun::argv().to_vec(); - if !graph.compile_exec_argv.is_empty() { - bun::append_options_env(graph.compile_exec_argv, &mut argv_list); + if !compile_exec_argv.is_empty() { + bun::append_options_env(compile_exec_argv, &mut argv_list); } // Store the full argv including user arguments @@ -1417,7 +1419,10 @@ pub mod command { .map(|a| a.to_vec().into_boxed_slice()) .collect(); - let entry_name = graph.entry_point().name.to_vec().into_boxed_slice(); + let entry_name = graph + .with_mut(|g| g.entry_point().name) + .to_vec() + .into_boxed_slice(); super::run_command::RunCommand::boot_standalone(ctx, entry_name, graph)?; Ok(()) } diff --git a/src/runtime/cli/multi_run.rs b/src/runtime/cli/multi_run.rs index 629fabd976c1..e55ca3536a37 100644 --- a/src/runtime/cli/multi_run.rs +++ b/src/runtime/cli/multi_run.rs @@ -801,9 +801,9 @@ pub(crate) fn run(ctx: &mut Command::ContextData) -> Result = this_transpiler.env; + // SAFETY: `env_ptr` is the process-lifetime loader singleton (see above). let event_loop = bun_event_loop::MiniEventLoop::init_global( - // SAFETY: env_ptr is the process-lifetime DotEnv loader; no other borrow of it is live yet. - Some(unsafe { &mut *env_ptr }), + Some(unsafe { bun_ptr::ParentRef::from_raw_mut(env_ptr) }), None, ); // --no-orphans: register the macOS kqueue parent watch on this MiniEventLoop diff --git a/src/runtime/cli/open.rs b/src/runtime/cli/open.rs index 26c0dac269cc..5e62fa1fcb07 100644 --- a/src/runtime/cli/open.rs +++ b/src/runtime/cli/open.rs @@ -528,12 +528,6 @@ impl EditorContext { pub fn detect_editor(&mut self, env: &mut dot_env::Loader) { let mut buf = PathBuffer::uninit(); - // Note: borrowck — `by_path_for_editor`/`by_fallback` tie `out`'s lifetime - // to `&'a mut buf`. On the `false` path NLL conservatively keeps `buf` borrowed - // (Polonius case). Re-borrow through a raw pointer at each call site; on a hit - // we return immediately so only one `&mut` is ever live. - let buf_ptr: *mut PathBuffer = &raw mut buf; - let mut out: &[u8] = b""; // first: choose from user preference if !self.name.is_empty() { @@ -547,11 +541,11 @@ impl EditorContext { // "vscode" if let Some(editor_) = Editor::by_name(bun_paths::basename(self.name)) { + let mut out: &[u8] = b""; if Editor::by_path_for_editor( env, editor_, - // SAFETY: see note above — exclusive per-call reborrow. - unsafe { &mut *buf_ptr }, + &mut buf, Fs::FileSystem::instance().top_level_dir, &mut out, ) { @@ -578,11 +572,11 @@ impl EditorContext { // EDITOR=code if let Some(editor_) = Editor::detect(env) { + let mut out: &[u8] = b""; if Editor::by_path_for_editor( env, editor_, - // SAFETY: see note above — exclusive per-call reborrow. - unsafe { &mut *buf_ptr }, + &mut buf, Fs::FileSystem::instance().top_level_dir, &mut out, ) { @@ -607,10 +601,10 @@ impl EditorContext { } // Don't know, so we will just guess based on what exists + let mut out: &[u8] = b""; if let Some(editor_) = Editor::by_fallback( env, - // SAFETY: see note above — exclusive per-call reborrow. - unsafe { &mut *buf_ptr }, + &mut buf, Fs::FileSystem::instance().top_level_dir, &mut out, ) { diff --git a/src/runtime/cli/outdated_command.rs b/src/runtime/cli/outdated_command.rs index 272b53c6aeec..1903529de935 100644 --- a/src/runtime/cli/outdated_command.rs +++ b/src/runtime/cli/outdated_command.rs @@ -89,26 +89,10 @@ impl OutdatedCommand { original_cwd: &[u8], manager: &mut PackageManager, ) -> Result<(), bun_core::Error> { - // Reshaped for borrowck — `load_from_cwd` would otherwise alias - // `PackageManager` with its `lockfile` field. Project disjoint - // raw pointers from the singleton first; `load_from_cwd` only reads - // `manager.options` / migration helpers and never re-borrows - // `manager.lockfile` through the `pm` argument. - let pm_ptr: *mut PackageManager = manager; let not_silent = manager.options.log_level != LogLevel::Silent; let log_ptr: *mut bun_ast::Log = manager.log; - // SAFETY: `lockfile` is the owned `Box` field on the singleton; - // no other live `&mut Lockfile` exists at this point. - let lockfile: &mut bun_install::lockfile::Lockfile = unsafe { &mut *(*pm_ptr).lockfile }; - // SAFETY: `manager.log` is set non-null by `PackageManager::init`. - let log = unsafe { &mut *log_ptr }; - match lockfile.load_from_cwd::( - // SAFETY: see comment above — `load_from_cwd` accesses `manager` - // fields disjoint from `lockfile`. - Some(unsafe { &mut *pm_ptr }), - log, - ) { + match manager.load_lockfile_from_cwd::() { LoadResult::NotFound => { if not_silent { Output::err_generic("missing lockfile, nothing outdated", ()); diff --git a/src/runtime/cli/pack_command.rs b/src/runtime/cli/pack_command.rs index 8e5290cb119c..69db13754e90 100644 --- a/src/runtime/cli/pack_command.rs +++ b/src/runtime/cli/pack_command.rs @@ -226,11 +226,11 @@ impl PackCommand { // `log` is non-null after `PackageManager::init()`. let log_ptr: *mut bun_ast::Log = manager.log; let manager_ptr: *mut PackageManager = manager; - // SAFETY: `manager_ptr`/`log_ptr` came from live `&mut`; reborrowed - // disjointly (`log` is a separate allocation from the manager fields - // `load_from_cwd` touches). - let load_from_disk_result = lockfile - .load_from_cwd::(Some(unsafe { &mut *manager_ptr }), unsafe { &mut *log_ptr }); + // SAFETY: `log_ptr` came from `manager.log`, non-null after + // `PackageManager::init()`; `*log_ptr` is a separate allocation from + // the manager fields `load_from_cwd` touches. + let load_from_disk_result = + lockfile.load_from_cwd::(Some(&mut *manager), unsafe { &mut *log_ptr }); let lockfile_ref: Option<&Lockfile> = match load_from_disk_result { LoadResult::Ok(ok) => Some(&*ok.lockfile), @@ -2129,7 +2129,11 @@ pub(crate) fn pack( // raw pointer so `run_package_script_foreground` can `&mut` it without // conflicting with our `&Transpiler` borrow. let transpiler_env: *mut bun_dotenv::Loader<'static> = this_transpiler.env; - manager.env_mut().map.put(b"npm_command", b"pack")?; + // SAFETY: `manager: &mut PackageManager`; `transpiler_env` above is a raw + // pointer, not a live reference, so this is the sole loader borrow. + unsafe { manager.env_mut() } + .map + .put(b"npm_command", b"pack")?; let (postpack_script, publish_script, postpublish_script, ran_scripts): ( Option>, @@ -3023,19 +3027,17 @@ pub(crate) fn pack( // Note: hoisted from repeated inline blocks to avoid 5x duplication of the // same `match err { MissingShell, OutOfMemory }` arms. Behavior identical. fn run_lifecycle_script( - ctx: &Context<'_>, + ctx: &mut Context<'_>, script: &[u8], name: &[u8], abs_workspace_path: &[u8], env: *mut bun_dotenv::Loader<'static>, silent: bool, ) -> Result<(), PackError> { - // Note: `ctx.command_ctx` and `env` are reborrowed via raw pointer - // because `run_package_script_foreground` needs `&mut` - // for `env.map.put()` while `ctx` only holds `&Context`. - // SAFETY: both are process-lifetime singletons; no concurrent `&mut` exists - // while a lifecycle script runs (single-threaded CLI dispatch). - let command_ctx = unsafe { &mut *std::ptr::from_ref(ctx.command_ctx).cast_mut() }; + // Note: `env` arrives as a raw pointer because + // `run_package_script_foreground` needs `&mut` for `env.map.put()` while + // the caller only holds `&Transpiler`. + let command_ctx = &mut *ctx.command_ctx; let use_system_shell = command_ctx.debug.use_system_shell; match RunCommand::run_package_script_foreground( command_ctx, diff --git a/src/runtime/cli/package_manager_command.rs b/src/runtime/cli/package_manager_command.rs index 1c359beb2cd1..70dd2c488b38 100644 --- a/src/runtime/cli/package_manager_command.rs +++ b/src/runtime/cli/package_manager_command.rs @@ -225,11 +225,6 @@ Learn more about these at https://bun.com/docs/cli/pm.\n"; } }; - // Reshaped for borrowck — `pm: &mut PackageManager`; - // several call sites need `pm` and `pm.lockfile` simultaneously. Hold a - // raw pointer for those re-entry points. - let pm_ptr: *mut PackageManager = pm; - let mut subcommand: &[u8] = if is_direct_whoami { b"whoami" } else { @@ -334,8 +329,6 @@ Learn more about these at https://bun.com/docs/cli/pm.\n"; let load_lockfile = pm.load_lockfile_from_cwd::(); Self::handle_load_lockfile_errors(&load_lockfile, log_level); - // SAFETY: pm_ptr is the unique owner; lockfile borrow released above. - let pm = unsafe { &mut *pm_ptr }; let _ = pm .lockfile .has_meta_hash_changed(false, pm.lockfile.packages.len())?; @@ -360,8 +353,6 @@ Learn more about these at https://bun.com/docs/cli/pm.\n"; let load_lockfile = pm.load_lockfile_from_cwd::(); Self::handle_load_lockfile_errors(&load_lockfile, log_level); - // SAFETY: pm_ptr is the unique owner; lockfile borrow released above. - let pm = unsafe { &mut *pm_ptr }; let _ = pm .lockfile .has_meta_hash_changed(true, pm.lockfile.packages.len())?; @@ -652,8 +643,8 @@ Learn more about these at https://bun.com/docs/cli/pm.\n"; // Reshaped for borrowck — // `detect_and_load_other_lockfile(&pm.lockfile, .cwd(), pm, ctx.log)` // is a self-referential split borrow. Derive both halves through - // `pm` (not the raw `pm_ptr`) so the outer borrow stays on the - // Stacked-Borrows stack. + // `pm`, so both raw pointers stay children of the outer borrow's + // tag under Tree Borrows. let pm_raw: *mut PackageManager = pm; // SAFETY: `pm.lockfile` is `Box` whose pointee lives in a // separate heap allocation; `&mut Lockfile` and `&mut PackageManager` diff --git a/src/runtime/cli/pm_trusted_command.rs b/src/runtime/cli/pm_trusted_command.rs index 2ae5568da938..548acc6dca4a 100644 --- a/src/runtime/cli/pm_trusted_command.rs +++ b/src/runtime/cli/pm_trusted_command.rs @@ -57,9 +57,9 @@ impl UntrustedCommand { Output::flush(); // Reshaped for borrowck — `LoadResult` returned by - // `load_lockfile_from_cwd` mutably borrows `pm.lockfile`, so all - // subsequent `pm` access goes through `pm_raw`. Same singleton pattern - // as `package_manager_command.rs::print_hash`. + // `load_lockfile_from_cwd` mutably borrows `pm`, so the + // `update_lockfile_if_needed` call below goes through `pm_raw`. Same + // singleton pattern as `package_manager_command.rs::print_hash`. let pm_raw: *mut PackageManager = pm; let log_level = pm.options.log_level; let load_lockfile = pm.load_lockfile_from_cwd::(); @@ -71,10 +71,6 @@ impl UntrustedCommand { // here. unsafe { update_lockfile_if_needed(&mut *pm_raw, &load_lockfile)? }; - // SAFETY: `load_lockfile` is not used past this point; `pm_raw` is the - // only path to the singleton for the rest of this fn (same as the - // original `pm`). - let pm: &mut PackageManager = unsafe { &mut *pm_raw }; let log: &mut bun_ast::Log = pm.log_mut(); let lockfile: &Lockfile = &pm.lockfile; @@ -289,8 +285,8 @@ impl TrustCommand { Self::error_expected_args(); } - // SAFETY: `pm_raw` is the singleton; `pm.log` set at init, non-null. - let log: *mut bun_ast::Log = unsafe { (*pm_raw).log }; + // SAFETY: `pm_raw` is the singleton, derived from `pm` above. + let log: &mut bun_ast::Log = unsafe { (*pm_raw).log_mut() }; // SAFETY: `pm_raw` singleton; read-only `lockfile` borrow for the discovery phase. let lockfile: &Lockfile = unsafe { &*(*pm_raw).lockfile }; @@ -378,9 +374,8 @@ impl TrustCommand { let folder_saved = node_modules_path.len(); let _ = node_modules_path.append(alias); - // SAFETY: `log` derived from `pm.log`; single-threaded CLI. let result = package_scripts.get_list( - unsafe { &mut *log }, + log, lockfile, &mut node_modules_path, alias, diff --git a/src/runtime/cli/pm_update_package_json.rs b/src/runtime/cli/pm_update_package_json.rs index a14d7e37f4a3..70339a522e3d 100644 --- a/src/runtime/cli/pm_update_package_json.rs +++ b/src/runtime/cli/pm_update_package_json.rs @@ -48,17 +48,15 @@ pub fn update_package_json_and_install(ctx: Context, subcommand: Subcommand) -> // typing in the dependency names // 3. Run the install command if cli.analyze { - // `ctx`/`cli` are stored as raw `*mut` because - // `BuildCommand::exec` holds `command::get()` (the same `ContextData`) across - // the `on_fetch` callback, and `DependenciesScanner.entry_points` owns a copy - // of `cli.positionals[1..]` for the duration of the scan; storing `&mut` here - // would assert exclusivity we don't have. - struct Analyzer { + // `ctx` is stored as a raw `*mut` because `BuildCommand::exec` takes its own + // `&mut ContextData` from `command::get()` — the same object — and holds it + // across the `on_fetch` callback, so a `&mut` field would be a live sibling. + struct Analyzer<'a> { ctx: *mut ContextData, - cli: *mut CommandLineArguments, + cli: &'a mut CommandLineArguments, subcommand: Subcommand, } - impl bun_bundler::bundle_v2::OnDependenciesAnalyze for Analyzer { + impl bun_bundler::bundle_v2::OnDependenciesAnalyze for Analyzer<'_> { fn on_analyze( &mut self, result: &mut DependenciesScannerResult<'_, '_>, @@ -90,21 +88,15 @@ pub fn update_package_json_and_install(ctx: Context, subcommand: Subcommand) -> v }); - // SAFETY: `this.cli` / `this.ctx` were set from live stack locals in - // `update_package_json_and_install` whose scope encloses the entire - // `BuildCommand::exec` call (and hence this callback). The bundler has - // finished reading `entry_points` before invoking `on_fetch`, and this - // callback never returns (`Global::exit` below), so forming fresh `&mut` - // here is exclusive for the remainder of the process. - let cli = unsafe { &mut *this.cli }; - cli.positionals = positionals.as_slice(); + this.cli.positionals = positionals.as_slice(); // SAFETY: `this.ctx` points to the `ctx` stack local in // `update_package_json_and_install`, whose frame outlives this // callback; `Global::exit` below makes this `&mut` exclusive for // the remainder of the process. let ctx = unsafe { &mut *this.ctx }; + let cli = this.cli.clone(); - update_package_json_and_install_and_cli(ctx, this.subcommand, cli.clone())?; + update_package_json_and_install_and_cli(ctx, this.subcommand, cli)?; Global::exit(0); } @@ -121,7 +113,7 @@ pub fn update_package_json_and_install(ctx: Context, subcommand: Subcommand) -> let mut analyzer = Analyzer { ctx: std::ptr::from_mut::(ctx), - cli: &raw mut cli, + cli: &mut cli, subcommand, }; diff --git a/src/runtime/cli/pm_version_command.rs b/src/runtime/cli/pm_version_command.rs index dd784389b430..d16627c6e252 100644 --- a/src/runtime/cli/pm_version_command.rs +++ b/src/runtime/cli/pm_version_command.rs @@ -159,7 +159,8 @@ impl PmVersionCommand { script_command, b"preversion", &package_json_dir, - pm.env_mut(), + // SAFETY: `pm: &mut PackageManager`; sole loader borrow. + unsafe { pm.env_mut() }, &[], silent, use_system_shell, @@ -239,7 +240,8 @@ impl PmVersionCommand { script_command, b"version", &package_json_dir, - pm.env_mut(), + // SAFETY: `pm: &mut PackageManager`; sole loader borrow. + unsafe { pm.env_mut() }, &[], silent, use_system_shell, @@ -260,7 +262,8 @@ impl PmVersionCommand { script_command, b"postversion", &package_json_dir, - pm.env_mut(), + // SAFETY: `pm: &mut PackageManager`; sole loader borrow. + unsafe { pm.env_mut() }, &[], silent, use_system_shell, diff --git a/src/runtime/cli/publish_command.rs b/src/runtime/cli/publish_command.rs index 715699dbf4bd..d4adc2db5868 100644 --- a/src/runtime/cli/publish_command.rs +++ b/src/runtime/cli/publish_command.rs @@ -464,13 +464,8 @@ impl<'a, const DIRECTORY_PUBLISH: bool> Context<'a, DIRECTORY_PUBLISH> { manager: &'a mut PackageManager, ) -> Result, FromWorkspaceError> { let mut lockfile = Lockfile::default(); - let manager_ptr: *mut PackageManager = manager; let log: &mut bun_ast::Log = manager.log_mut(); - // SAFETY: `manager_ptr` was just derived from `manager: &'a mut PackageManager`; - // `log` borrows the disjoint `.log` field, so the re-derived `&mut` - // never touches memory the live `log` borrow covers. - let load_from_disk_result = - lockfile.load_from_cwd::(Some(unsafe { &mut *manager_ptr }), log); + let load_from_disk_result = lockfile.load_from_cwd::(Some(&mut *manager), log); let lockfile_ref: Option<&Lockfile> = match load_from_disk_result { LoadResult::Ok(ok) => Some(&*ok.lockfile), @@ -507,18 +502,11 @@ impl<'a, const DIRECTORY_PUBLISH: bool> Context<'a, DIRECTORY_PUBLISH> { // Note: capture the package.json path before constructing // `pack::Context` so the `&mut PackageManager` borrow doesn't conflict. - // SAFETY: `manager_ptr` came from `&'a mut PackageManager`. - let abs_pkg_json = bun_core::ZBox::from_bytes( - unsafe { &*manager_ptr } - .original_package_json_path - .as_bytes(), - ); + let abs_pkg_json = + bun_core::ZBox::from_bytes(manager.original_package_json_path.as_bytes()); let mut pack_ctx = pack::Context { - // SAFETY: `manager_ptr` came from `&'a mut PackageManager`; - // `lockfile_ref` borrows the local `lockfile`, not the manager, - // so the re-derived `&mut` is the only live manager borrow. - manager: unsafe { &mut *manager_ptr }, + manager, command_ctx: ctx, lockfile: lockfile_ref, bundled_deps: Vec::new(), @@ -555,7 +543,6 @@ impl PublishCommand { } }; drop(original_cwd); - let manager_ptr: *mut PackageManager = manager; if cli.positionals.len() > 1 { let context = match Context::::from_tarball_path( @@ -587,8 +574,8 @@ impl PublishCommand { ); } FromTarballError::InvalidPackageJSON => { - // SAFETY: `manager.log` is set once at init. - let _ = unsafe { &mut *(*manager_ptr).log } + let _ = PackageManager::get() + .log_mut() .print(std::ptr::from_mut(Output::error_writer())); Output::err_generic("failed to parse tarball package.json", ()); } @@ -714,23 +701,17 @@ impl PublishCommand { .put(b"npm_command", b"publish") .map_err(|_| err!(OutOfMemory))?; - // Note: reshaped for borrowck — `command_ctx: &mut ContextData` - // is held by `context`; `run_package_script_foreground` needs - // `&mut ContextData` too. Re-derive from the raw pointer. - let cmd_ctx_ptr: *mut crate::cli::command::ContextData = context.command_ctx; - if let Some(publish_script) = &context.publish_script { + let use_system_shell = context.command_ctx.debug.use_system_shell; if let Err(e) = Run::run_package_script_foreground( - // SAFETY: see above. - unsafe { &mut *cmd_ctx_ptr }, + &mut *context.command_ctx, publish_script, b"publish", &abs_workspace_path, script_env, &[], context.manager.options.log_level == LogLevel::Silent, - // SAFETY: see above. - unsafe { &*cmd_ctx_ptr }.debug.use_system_shell, + use_system_shell, ) { if e == err!("MissingShell") { Output::err_generic( @@ -744,17 +725,16 @@ impl PublishCommand { } if let Some(postpublish_script) = &context.postpublish_script { + let use_system_shell = context.command_ctx.debug.use_system_shell; if let Err(e) = Run::run_package_script_foreground( - // SAFETY: see above. - unsafe { &mut *cmd_ctx_ptr }, + &mut *context.command_ctx, postpublish_script, b"postpublish", &abs_workspace_path, script_env, &[], context.manager.options.log_level == LogLevel::Silent, - // SAFETY: see above. - unsafe { &*cmd_ctx_ptr }.debug.use_system_shell, + use_system_shell, ) { if e == err!("MissingShell") { Output::err_generic( diff --git a/src/runtime/cli/run_command.rs b/src/runtime/cli/run_command.rs index 53bf18368aeb..2e8167c2d3ee 100644 --- a/src/runtime/cli/run_command.rs +++ b/src/runtime/cli/run_command.rs @@ -198,18 +198,17 @@ Full documentation is available at https://bun.com/docs/cli/run pub fn find_shell(path: &[u8], cwd: &[u8]) -> Option<&'static ZStr> { // Process-lifetime; written exactly once on the CLI thread. // PORTING.md §Global mutable state: scratch buffer behind - // a `Once` gate → RacyCell. - static SHELL_BUF: bun_core::RacyCell = - bun_core::RacyCell::new(PathBuffer::ZEROED); + // a `Once` gate → JsCell. + static SHELL_BUF: bun_jsc::JsCell = bun_jsc::JsCell::new(PathBuffer::ZEROED); static ONCE: bun_core::Once> = bun_core::Once::new(); ONCE.call(|| { - // SAFETY: single-writer (Once gate), process-lifetime storage, - // CLI is single-threaded at this point. - let buf = unsafe { &mut *SHELL_BUF.get() }; - let len = Self::find_shell_impl(path, cwd, buf)?; - buf[len] = 0; - // SAFETY: `buf[len] == 0` written above; SHELL_BUF is `'static`. - Some(ZStr::from_buf(&buf[..], len)) + let len = SHELL_BUF.with_mut(|buf| { + let len = Self::find_shell_impl(path, cwd, buf)?; + buf[len] = 0; + Some(len) + })?; + // `buf[len] == 0` written above; SHELL_BUF is `'static`. + Some(ZStr::from_buf(&SHELL_BUF.get()[..], len)) }) } @@ -305,14 +304,15 @@ Full documentation is available at https://bun.com/docs/cli/run } if !use_system_shell { - // SAFETY: `MiniEventLoop` stores `env` as a raw `*mut`; the loader - // outlives the call (process-lifetime in `configure_env_for_run`). - // Erase the loader's borrowed lifetime to `'static` for the - // singleton handoff. + // SAFETY: the loader outlives the call (process-lifetime in + // `configure_env_for_run`). Erase the borrowed lifetime to `'static` + // for the singleton handoff. let mini = bun_event_loop::MiniEventLoop::init_global( Some(unsafe { - &mut *std::ptr::from_mut::>(env) - .cast::>() + bun_ptr::ParentRef::from_raw_mut( + std::ptr::from_mut::>(env) + .cast::>(), + ) }), Some(cwd), ); @@ -398,8 +398,10 @@ Full documentation is available at https://bun.com/docs/cli/run // SAFETY: same lifetime erasure as the `!use_system_shell` // branch above — `env` outlives the mini event loop. Some(unsafe { - &mut *::core::ptr::from_mut::>(env) - .cast::>() + bun_ptr::ParentRef::from_raw_mut( + ::core::ptr::from_mut::>(env) + .cast::>(), + ) }), None, ), @@ -896,11 +898,10 @@ Full documentation is available at https://bun.com/docs/cli/run bundle.run_env_loader(bundle.options.env.disable_default_env_files)?; let top_level_dir: &[u8] = ctx.args.absolute_working_dir.as_deref().unwrap_or(b""); + // SAFETY: `bundle.env` is the process-lifetime loader singleton set in + // `Transpiler::init`; no `&mut` to it is live here. let mini = bun_event_loop::MiniEventLoop::init_global( - // SAFETY: `bundle.env` points to the process-lifetime DotEnv - // singleton (set by `Transpiler::init`); erasing the borrowed - // lifetime mirrors the `run_package_script_foreground` handoff. - Some(unsafe { &mut *bundle.env.cast::>() }), + Some(unsafe { bun_ptr::ParentRef::from_raw_mut(bundle.env.cast()) }), None, ); // SAFETY: `init_global` returns the thread-local singleton; single- @@ -1096,10 +1097,11 @@ Full documentation is available at https://bun.com/docs/cli/run // SAFETY: `RUN` is the process-global singleton; // written exactly once here on the main thread before the API-lock // trampoline reads it, never freed (`global_exit` ends the process). + // `vm_ptr` is the leaked main-thread VM (outlives `Run`). unsafe { RUN.get().write(Run { ctx: std::ptr::from_mut::(ctx), - vm: vm_ptr, + vm: Some(bun_ptr::BackRef::from_raw(vm_ptr)), entry_path: entry_ptr, }); } @@ -1108,20 +1110,13 @@ Full documentation is available at https://bun.com/docs/cli/run // from `self.ctx` to drive the hot-reloader enable. vm.hot_reload = ctx.debug.hot_reload as u8; - extern "C" fn trampoline(ctx: *mut c_void) { - // SAFETY: `ctx` is `&mut RUN` passed through `holdAPILock`'s - // opaque slot; the API lock is held for the full call so no - // other thread touches the VM. - let this = unsafe { &mut *ctx.cast::() }; - this.start(); - } // SAFETY: `vm.global` set in `init`; `vm()` borrows the JSC VM for - // the API-lock FFI call. `&raw mut RUN` yields a stable raw pointer + // the API-lock FFI call. `RUN.get()` yields a stable raw pointer // to the static. #[allow(deprecated)] vm.global() .vm() - .hold_api_lock(RUN.get().cast::(), trampoline); + .hold_api_lock(RUN.get().cast::(), run_api_lock_trampoline); // `Run::start` never returns (ends in `global_exit`); this is dead // code kept so the type unifies with the `?`-early-return above. @@ -1135,7 +1130,7 @@ Full documentation is available at https://bun.com/docs/cli/run pub(crate) fn boot_standalone( ctx: &mut ContextData, entry_path: Box<[u8]>, - graph: &mut bun_standalone_graph::Graph, + graph: &'static bun_jsc::JsCell, ) -> Result<(), bun_core::Error> { use bun_standalone_graph::StandaloneModuleGraph::Flags as GraphFlags; @@ -1145,7 +1140,12 @@ Full documentation is available at https://bun.com/docs/cli/run // Load bunfig.toml unless disabled by compile flags. Config loading // with execArgv is handled earlier in `Command::start` via `init()`. - if !ctx.debug.loaded_bunfig && !graph.flags.contains(GraphFlags::DISABLE_AUTOLOAD_BUNFIG) { + if !ctx.debug.loaded_bunfig + && !graph + .get() + .flags + .contains(GraphFlags::DISABLE_AUTOLOAD_BUNFIG) + { arguments::load_config_path( CommandTag::RunCommand, true, @@ -1163,7 +1163,7 @@ Full documentation is available at https://bun.com/docs/cli/run // SAFETY: `graph` lives in the process-global INSTANCE static; never // freed (`global_exit` ends the process before any deinit). let graph_dyn: &'static dyn bun_resolver::StandaloneModuleGraph = unsafe { - &*(std::ptr::from_ref::(graph) + &*(graph.as_ptr().cast_const() as *const (dyn bun_resolver::StandaloneModuleGraph + 'static)) }; let vm_ptr = VirtualMachine::init_with_module_graph(bun_jsc::virtual_machine::Options { @@ -1207,7 +1207,7 @@ Full documentation is available at https://bun.com/docs/cli/run b.options.serve_plugins = ctx.args.serve_plugins.take().map(Vec::into_boxed_slice); b.options.bunfig_path = ::core::mem::take(&mut ctx.args.bunfig_path); - crate::run_main::apply_standalone_runtime_flags(b, graph); + crate::run_main::apply_standalone_runtime_flags(b, graph.get()); b.configure_defines().is_ok() }; @@ -1237,26 +1237,21 @@ Full documentation is available at https://bun.com/docs/cli/run // SAFETY: `RUN` is the process-global singleton; // written exactly once here on the main thread before the API-lock // trampoline reads it, never freed (`global_exit` ends the process). + // `vm_ptr` is the leaked main-thread VM (outlives `Run`). unsafe { RUN.get().write(Run { ctx: std::ptr::from_mut::(ctx), - vm: vm_ptr, + vm: Some(bun_ptr::BackRef::from_raw(vm_ptr)), entry_path: entry_ptr, }); } - extern "C" fn trampoline(ctx: *mut c_void) { - // SAFETY: `ctx` is `&mut RUN` passed through `holdAPILock`'s - // opaque slot; the API lock is held for the full call. - let this = unsafe { &mut *ctx.cast::() }; - this.start(); - } // SAFETY: `vm.global` set in `init`; `vm()` borrows the JSC VM for // the API-lock FFI call. #[allow(deprecated)] vm.global() .vm() - .hold_api_lock(RUN.get().cast::(), trampoline); + .hold_api_lock(RUN.get().cast::(), run_api_lock_trampoline); // `Run::start` never returns; dead code for `?`-early-return type unify. Ok(()) @@ -1275,7 +1270,9 @@ pub struct Run { /// pointer here so [`Run::start`] can read profiler / preconnect / /// hot-reload flags under the API lock without re-threading every field. ctx: *mut ContextData, - vm: *mut VirtualMachine, + /// BACKREF — the boxed-and-leaked main-thread VM, which outlives `Run`. + /// `None` only in the zero-init `RUN` static, before `boot()` writes it. + vm: Option>, /// Heap bytes (from `boot`'s `heap::alloc`) or a borrow into the standalone graph's /// `entryPoint().name` (from `boot_standalone`). Either way the bytes live /// for the process — `Run::start` never returns — so a raw `*const [u8]` @@ -1290,7 +1287,7 @@ pub struct Run { // the `holdAPILock` trampoline can re-derive `&mut Run` from the static. static RUN: bun_core::RacyCell = bun_core::RacyCell::new(Run { ctx: ::core::ptr::null_mut(), - vm: ::core::ptr::null_mut(), + vm: None, entry_path: ::core::ptr::slice_from_raw_parts(::core::ptr::null(), 0), }); @@ -1302,6 +1299,14 @@ static RUN: bun_core::RacyCell = bun_core::RacyCell::new(Run { // callback's write and `start()`'s reads never overlap a `&mut`. static ANY_UNHANDLED: AtomicBool = AtomicBool::new(false); +/// `holdAPILock`'s opaque `ctx` slot is always `RUN`. One typed deref here, +/// then a safe method call; the API lock is held for the whole call. +extern "C" fn run_api_lock_trampoline(ctx: *mut c_void) { + // SAFETY: `ctx` is `RUN.get()`, written by `boot`/`boot_standalone` + // before registration and never freed (`global_exit` ends the process). + unsafe { bun_core::callback_ctx::(ctx) }.start(); +} + impl Run { /// `onUnhandledRejectionBeforeClose` — record that *something* rejected so /// `start()` sets a non-zero exit code, then route through the VM's @@ -1326,10 +1331,9 @@ impl Run { fn Bun__ExposeNodeModuleGlobals(global: *const JSGlobalObject); fn JSC__JSGlobalObject__addGc(global: *const JSGlobalObject); } - // SAFETY: `self.vm`/`self.ctx` are process-lifetime; written by - // `boot()` before the API-lock trampoline runs. - let vm = unsafe { &*self.vm }; - // SAFETY: `self.ctx` is process-lifetime; see comment on `vm` above. + let vm = self.vm.as_ref().expect("boot() ran").get(); + // SAFETY: `self.ctx` is process-lifetime; written by `boot()` before + // the API-lock trampoline runs. let ro = unsafe { &(*self.ctx).runtime_options }; if !ro.eval.script.is_empty() { // SAFETY: FFI; `vm.global` is live for the VM lifetime. @@ -1350,7 +1354,7 @@ impl Run { // SAFETY: `self.vm` is the boxed-and-leaked main-thread VM; `self.ctx` // is the CLI's process-lifetime `ContextData`. Both are written by // `boot()`/`boot_standalone()` before the API-lock trampoline runs. - let vm = unsafe { &mut *self.vm }; + let vm = unsafe { &mut *self.vm.expect("boot() ran").as_ptr() }; // SAFETY: `self.ctx` is process-lifetime; see comment on `vm` above. let ctx = unsafe { &*self.ctx }; // SAFETY: `entry_path` is process-lifetime (heap from `heap::alloc` @@ -1377,8 +1381,7 @@ impl Run { interval: opts.interval, }); bun_jsc::bun_cpu_profiler::set_sampling_interval(opts.interval); - // SAFETY: `vm.jsc_vm` set in `init`. - bun_jsc::bun_cpu_profiler::start_cpu_profiler(unsafe { &mut *vm.jsc_vm }); + bun_jsc::bun_cpu_profiler::start_cpu_profiler(vm.jsc_vm_mut()); bun_analytics::features::cpu_profile.fetch_add(1, Ordering::Relaxed); } @@ -1475,7 +1478,7 @@ impl Run { // (process-lifetime); it outlives the leaked reloader. unsafe { bun_jsc::hot_reloader::HotReloader::enable_hot_module_reloading( - self.vm, + self.vm.expect("boot() ran").as_ptr(), Some(entry), ) } @@ -1485,7 +1488,7 @@ impl Run { // (process-lifetime); it outlives the leaked reloader. unsafe { bun_jsc::hot_reloader::WatchReloader::enable_hot_module_reloading( - self.vm, + self.vm.expect("boot() ran").as_ptr(), Some(entry), ) } @@ -1504,11 +1507,12 @@ impl Run { match vm.load_entry_point(entry) { Ok(promise) => { - // SAFETY: `promise` is a live GC cell returned by the module loader. - let promise = unsafe { &mut *promise }; + // SAFETY: `promise` is a live GC cell returned by the module + // loader. Shared borrow only: `uncaught_exception` below runs + // user JS, which may re-enter and touch this same promise. + let promise = unsafe { &*promise }; if promise.status() == PromiseStatus::Rejected { - // SAFETY: `vm.jsc_vm` set in `init`; FFI takes `*mut`. - let result = promise.result(unsafe { &mut *vm.jsc_vm }); + let result = promise.result(vm.jsc_vm()); let global = vm.global; // SAFETY: `global` valid for VM lifetime. let handled = vm.uncaught_exception(unsafe { &*global }, result, true); @@ -1529,8 +1533,7 @@ impl Run { } } - // SAFETY: `vm.jsc_vm` set in `init`. - let _ = promise.result(unsafe { &mut *vm.jsc_vm }); + let _ = promise.result(vm.jsc_vm()); if log_has_msgs(vm) { dump_build_error(vm); @@ -1683,9 +1686,7 @@ fn log_clear_msgs(vm: &mut VirtualMachine) { )] fn dump_build_error(vm: &mut VirtualMachine) { Output::flush(); - if let Some(log) = vm.log { - // SAFETY: `vm.log` set in `init`; single-threaded CLI. - let log = unsafe { &mut *log.as_ptr() }; + if let Some(log) = vm.log_mut() { let _ = log.print(std::ptr::from_mut::( Output::error_writer_buffered(), )); @@ -2092,17 +2093,18 @@ impl RunCommand { if bun_core::FeatureFlags::WINDOWS_BUNX_FAST_PATH && executable.ends_with(b".exe") { debug_assert!(paths::is_absolute(executable)); - // SAFETY: `DIRECT_LAUNCH_BUFFER` is a process-lifetime static used - // single-threaded from CLI dispatch. The returned slice points into - // it; we keep the borrow scoped until `try_launch` consumes it. - let buf = unsafe { &mut *bunx_fast_path_buffers::DIRECT_LAUNCH_BUFFER.get() }; - let w = strings::to_nt_path(buf, executable); - let w_len = w.len(); - debug_assert!(w_len > sys::windows::NT_OBJECT_PREFIX.len() + b".exe".len()); - let new_len = w_len + b".bunx".len() - b".exe".len(); - let bunx = bun_core::w!("bunx"); - buf[new_len - bunx.len()..new_len].copy_from_slice(bunx); - buf[new_len] = 0; + // The borrow ends before `try_launch`, which can re-enter this + // buffer through the shim's `direct_launch_callback`. + let new_len = bunx_fast_path_buffers::DIRECT_LAUNCH_BUFFER.with_mut(|buf| { + let w = strings::to_nt_path(buf, executable); + let w_len = w.len(); + debug_assert!(w_len > sys::windows::NT_OBJECT_PREFIX.len() + b".exe".len()); + let new_len = w_len + b".bunx".len() - b".exe".len(); + let bunx = bun_core::w!("bunx"); + buf[new_len - bunx.len()..new_len].copy_from_slice(bunx); + buf[new_len] = 0; + new_len + }); BunXFastPath::try_launch(ctx, new_len, env, passthrough); } @@ -2169,8 +2171,10 @@ impl RunCommand { Some(unsafe { // SAFETY: env loader is process-lifetime; erase // borrowed lifetime for the singleton handoff. - &mut *::core::ptr::from_mut::>(env) - .cast::>() + bun_ptr::ParentRef::from_raw_mut( + ::core::ptr::from_mut::>(env) + .cast::>(), + ) }), None, ), @@ -2651,21 +2655,20 @@ impl RunCommand { // ── Windows .bunx fast-path ────────────────────────────────────────── #[cfg(windows)] if bun_core::FeatureFlags::WINDOWS_BUNX_FAST_PATH { - // SAFETY: process-lifetime static, single-threaded CLI dispatch. - let buf = unsafe { &mut *bunx_fast_path_buffers::DIRECT_LAUNCH_BUFFER.get() }; // NT object-manager prefix (`\??\`), NOT the Win32 long-path - // `\\?\` — `try_launch` hands this to NtCreateFile. - let root = bun_core::w!("\\??\\"); - buf[..root.len()].copy_from_slice(root); - let cwd_len = unsafe { - sys::windows::kernel32::GetCurrentDirectoryW( - (buf.len() - 4) as u32, - buf.as_mut_ptr().add(root.len()), - ) - } as usize; - 'try_bunx_file: { + // `\\?\` — `try_launch` hands this to NtCreateFile. The borrow ends + // before `try_launch`, which can re-enter this buffer via the shim. + let bunx_len = bunx_fast_path_buffers::DIRECT_LAUNCH_BUFFER.with_mut(|buf| { + let root = bun_core::w!("\\??\\"); + buf[..root.len()].copy_from_slice(root); + let cwd_len = unsafe { + sys::windows::kernel32::GetCurrentDirectoryW( + (buf.len() - 4) as u32, + buf.as_mut_ptr().add(root.len()), + ) + } as usize; if cwd_len == 0 { - break 'try_bunx_file; + return None; } let mut ptr = root.len() + cwd_len; let prefix = bun_core::w!("\\node_modules\\.bin\\"); @@ -2678,7 +2681,9 @@ impl RunCommand { buf[ptr..ptr + ext.len()].copy_from_slice(ext); ptr += ext.len(); buf[ptr] = 0; - + Some(ptr) + }); + if let Some(ptr) = bunx_len { let passthrough: Vec> = ctx.passthrough.clone(); BunXFastPath::try_launch(ctx, ptr, env_loader, &passthrough); } @@ -2690,8 +2695,7 @@ impl RunCommand { // search the whole stitched PATH. { let _ = force_using_bun; - // SAFETY: `Transpiler::init` always sets `fs`; resolver-cache lifetime. - let fs = unsafe { &mut *this_transpiler.fs }; + let fs = this_transpiler.fs_mut(); let top_level_dir = fs.top_level_dir; let path = env_loader.get(b"PATH").unwrap_or(b""); let mut path_for_which = path; @@ -3676,14 +3680,21 @@ impl RunCommand { let value = unsafe { &**entry.1 }; // SAFETY: entries_mutex held; `Transpiler::fs` is the // non-null process-static singleton. - if unsafe { value.kind(&raw mut (*this_transpiler.fs).fs, true) } - == bun_resolver::fs::EntryKind::File + if unsafe { + value.kind( + bun_ptr::ParentRef::from_raw_mut( + &raw mut (*this_transpiler.fs).fs, + ), + true, + ) + } == bun_resolver::fs::EntryKind::File { if !has_copied { - path_buf[..value.dir.len()].copy_from_slice(value.dir); - dir_slice_len = value.dir.len(); - if !strings::ends_with_char_or_is_zero_length(value.dir, SEP) { - dir_slice_len = value.dir.len() + 1; + let dir = value.dir(); + path_buf[..dir.len()].copy_from_slice(dir); + dir_slice_len = dir.len(); + if !strings::ends_with_char_or_is_zero_length(dir, SEP) { + dir_slice_len = dir.len() + 1; } has_copied = true; } @@ -3739,8 +3750,14 @@ impl RunCommand { && !strings::contains(name, b".d.cts") // SAFETY: entries_mutex held; `Transpiler::fs` is the // non-null process-static singleton. - && unsafe { value.kind(&raw mut (*this_transpiler.fs).fs, true) } - == bun_resolver::fs::EntryKind::File + && unsafe { + value.kind( + bun_ptr::ParentRef::from_raw_mut( + &raw mut (*this_transpiler.fs).fs, + ), + true, + ) + } == bun_resolver::fs::EntryKind::File { // SAFETY: `Transpiler::fs` is the non-null process-static singleton. let Ok(appended) = @@ -3925,9 +3942,9 @@ pub enum BunXFastPath {} mod bunx_fast_path_buffers { use super::*; // PORTING.md §Global mutable state: Windows-only single-thread CLI scratch - // buffers (bunx fast-path runs once on the main thread) → RacyCell. - pub(super) static DIRECT_LAUNCH_BUFFER: bun_core::RacyCell = - bun_core::RacyCell::new(WPathBuffer::ZEROED); + // buffers (bunx fast-path runs once on the main thread) → JsCell. + pub(super) static DIRECT_LAUNCH_BUFFER: bun_jsc::JsCell = + bun_jsc::JsCell::new(WPathBuffer::ZEROED); } impl BunXFastPath { @@ -4016,10 +4033,9 @@ impl BunXFastPath { return; } - // SAFETY: process-lifetime static, single-threaded CLI dispatch. - let direct_launch_buffer = - unsafe { &mut *bunx_fast_path_buffers::DIRECT_LAUNCH_BUFFER.get() }; - let (path_to_use, command_line) = direct_launch_buffer.split_at_mut(path_len); + // No `&mut WPathBuffer` may span `try_startup_from_bun_js` below: the + // shim re-enters `direct_launch_callback`, which touches this cell. + let path_to_use = &bunx_fast_path_buffers::DIRECT_LAUNCH_BUFFER.get()[..path_len]; bun_core::scoped_log!( BUNX_FAST_PATH_LOG, @@ -4051,12 +4067,15 @@ impl BunXFastPath { }; let mut i: usize = 0; - for arg in passthrough { - // Add space separator before each argument - command_line[i] = b' ' as u16; - i += 1; - i += Self::append_windows_argument(&mut command_line[i..], arg); - } + bunx_fast_path_buffers::DIRECT_LAUNCH_BUFFER.with_mut(|buf| { + let command_line = &mut buf[path_len..]; + for arg in passthrough { + // Add space separator before each argument + command_line[i] = b' ' as u16; + i += 1; + i += Self::append_windows_argument(&mut command_line[i..], arg); + } + }); // `direct_launch_callback` → // `Run::boot` reads `vm.argv = ctx.passthrough`, so the assignment must // happen before the shim may call back. Current callers pass a clone of @@ -4066,11 +4085,17 @@ impl BunXFastPath { let env_block = env.map.write_windows_env_block(); + // Raw derivations straight off the cell (no `&mut` retag) so the + // shim's re-entrant `direct_launch_callback` cannot invalidate them. + let base: *mut u16 = bunx_fast_path_buffers::DIRECT_LAUNCH_BUFFER.as_ptr().cast(); let run_ctx = bun_install::windows_shim::bun_shim_impl::FromBunRunContext { handle, - base_path: path_to_use[4..].as_mut_ptr(), - base_path_len: path_to_use.len() - 4, - arguments: command_line.as_mut_ptr(), + // SAFETY: `4 <= path_len` and `path_len` is in bounds of the buffer + // (the NT prefix plus a path were written into it above). + base_path: unsafe { base.add(4) }, + base_path_len: path_len - 4, + // SAFETY: as above — `path_len` is in bounds of the buffer. + arguments: unsafe { base.add(path_len) }, arguments_len: i, force_use_bun: ctx.debug.run_in_bun, direct_launch_with_bun_js: Self::direct_launch_callback, @@ -4091,24 +4116,21 @@ impl BunXFastPath { #[cfg(windows)] fn direct_launch_callback(wpath: &mut [u16], ctx: bun_options_types::context::Context<'_>) { - // SAFETY: process-lifetime static, single-threaded CLI dispatch. - // `try_launch` (still on the call stack) holds live `&mut [u16]` - // reborrows (`path_to_use`/`command_line`) and raw pointers - // (`run_ctx.base_path`/`arguments`) into this same UnsafeCell. - // Materialising a fresh `&mut WPathBuffer` here would push a Unique - // tag over the whole buffer and pop those tags under Stacked Borrows. - // Derive the byte slice directly from the raw `*mut WPathBuffer` so no - // intermediate `&mut` retag covers the caller's borrows. + // `try_launch` (still on the call stack) holds raw pointers + // (`run_ctx.base_path`/`arguments`) into this same cell, so derive the + // byte slice from the raw pointer — never a `&mut WPathBuffer`. // WPathBuffer is `#[repr(transparent)] [u16; PATH_MAX_WIDE]` — // reinterpret as `[u8; 2N]` for the UTF-16→UTF-8 transcoder's output. + // SAFETY: process-lifetime static, single-threaded CLI dispatch. let out_buf = unsafe { - let raw = bunx_fast_path_buffers::DIRECT_LAUNCH_BUFFER.get(); + let raw = bunx_fast_path_buffers::DIRECT_LAUNCH_BUFFER.as_ptr(); ::core::slice::from_raw_parts_mut(raw.cast::(), bun_paths::PATH_MAX_WIDE * 2) }; let utf8 = strings::convert_utf16_to_utf8_in_buffer(out_buf, wpath); if let Err(err) = RunCommand::boot(ctx, utf8.to_vec().into_boxed_slice(), None) { - // SAFETY: `ctx.log` was set in `create_context_data`. - let _ = unsafe { &mut *ctx.log }.print(std::ptr::from_mut(Output::error_writer())); + // SAFETY: `ctx.log` was set in `create_context_data`; no other + // borrow of that `Log` is live here. + let _ = unsafe { ctx.log_mut() }.print(std::ptr::from_mut(Output::error_writer())); Output::err( err, "Failed to run bin \"{}\"", diff --git a/src/runtime/cli/scan_command.rs b/src/runtime/cli/scan_command.rs index 0a0123d2a793..b6f67800fb20 100644 --- a/src/runtime/cli/scan_command.rs +++ b/src/runtime/cli/scan_command.rs @@ -2,7 +2,7 @@ use crate::Command; use bun_core::{Global, Output, err}; use bun_install::lockfile::LoadResult; use bun_install::package_manager::{self, security_scanner}; -use bun_install::{CommandLineArguments, Lockfile, PackageManager, Subcommand}; +use bun_install::{CommandLineArguments, PackageManager, Subcommand}; pub struct ScanCommand; @@ -54,36 +54,19 @@ impl ScanCommand { ); Output::flush(); - // Reshaped for borrowck — `manager.lockfile.load_from_cwd(&mut self, - // Some(manager), log)` would alias `&mut *manager.lockfile` with `&mut *manager`. - // Project disjoint raw pointers from the singleton first; `load_from_cwd` only - // reads `manager.options`/migration helpers and never re-borrows `manager.lockfile`. - { - let pm_ptr: *mut PackageManager = manager; - // SAFETY: `manager.log` is set non-null by `PackageManager::init`. - let log: &mut bun_ast::Log = unsafe { &mut *(*pm_ptr).log }; - // SAFETY: `lockfile` is the owned `Box` field on the singleton; - // no other live `&mut Lockfile` exists at this point. - let lockfile: &mut Lockfile = unsafe { &mut *(*pm_ptr).lockfile }; - match lockfile.load_from_cwd::( - // SAFETY: see comment above — `load_from_cwd` accesses `manager` - // fields disjoint from `lockfile`. - Some(unsafe { &mut *pm_ptr }), - log, - ) { - LoadResult::NotFound => { - Output::err_generic( - "Lockfile not found. Run 'bun install' first to generate a lockfile.", - (), - ); - Global::exit(1); - } - LoadResult::Err(e) => { - Output::err_generic("Error loading lockfile: {s}", (e.value.name(),)); - Global::exit(1); - } - LoadResult::Ok(_) => {} + match manager.load_lockfile_from_cwd::() { + LoadResult::NotFound => { + Output::err_generic( + "Lockfile not found. Run 'bun install' first to generate a lockfile.", + (), + ); + Global::exit(1); + } + LoadResult::Err(e) => { + Output::err_generic("Error loading lockfile: {s}", (e.value.name(),)); + Global::exit(1); } + LoadResult::Ok(_) => {} } let security_scan_results = diff --git a/src/runtime/cli/test/Scanner.rs b/src/runtime/cli/test/Scanner.rs index 1be66233f18a..36d49cfc362f 100644 --- a/src/runtime/cli/test/Scanner.rs +++ b/src/runtime/cli/test/Scanner.rs @@ -69,7 +69,7 @@ impl PartialEq for ScanError { #[repr(transparent)] struct ScannerDirIter<'a>(*mut Scanner<'a>); impl<'a> DirEntryIterator for ScannerDirIter<'a> { - fn next(&self, entry: &mut fs::Entry, fd: Fd) { + fn next(&self, entry: &fs::Entry, fd: Fd) { // SAFETY: `self.0` is `&mut Scanner` for the duration of // `read_directory_with_iterator`; no other live `&mut` alias exists // while the resolver walks entries. @@ -196,7 +196,7 @@ impl<'a> Scanner<'a> { for entry_ptr in entry_ptrs { // SAFETY: `EntryMap` stores `*mut Entry` into the // process-static `EntryStore`; valid for `'static`. - self.next(unsafe { &mut *entry_ptr }, fd); + self.next(unsafe { &*entry_ptr }, fd); } } } @@ -363,13 +363,13 @@ impl<'a> Scanner<'a> { && !self.matches_path_ignore_pattern(name) } - pub fn next(&mut self, entry: &mut fs::Entry, fd: Fd) { + pub fn next(&mut self, entry: &fs::Entry, fd: Fd) { let name = entry.base_lowercase(); self.has_iterated = true; // SAFETY: `self.fs` is the process singleton. let real_fs = unsafe { &raw mut (*self.fs).fs }; // SAFETY: caller holds `entries_mutex`; the direct path is single-threaded. - match unsafe { entry.kind(real_fs, true) } { + match unsafe { entry.kind(bun_ptr::ParentRef::from_raw(real_fs), true) } { fs::EntryKind::Dir => { if (!name.is_empty() && name[0] == b'.') || name == b"node_modules" { return; @@ -389,7 +389,7 @@ impl<'a> Scanner<'a> { // Prune ignored directory trees early so we never traverse them. if !self.path_ignore_patterns.is_empty() { - let parts: [&[u8]; 2] = [entry.dir, entry.base()]; + let parts: [&[u8]; 2] = [entry.dir(), entry.base()]; // reshaped for borrowck — drop the &mut borrow from // abs_buf and reborrow open_dir_buf immutably so &self methods // can be called with the slice. @@ -412,12 +412,12 @@ impl<'a> Scanner<'a> { // SAFETY: StringOrTinyString is repr(C) POD ([u8;31] + u8) with // no Drop. Upstream type lacks Clone/Copy, so bitwise-copy here. name: unsafe { core::ptr::read(&raw const entry.base_) }, - dir_path: entry.dir, + dir_path: entry.dir(), }); } fs::EntryKind::File => { // already seen it! - if !entry.abs_path.is_empty() { + if !entry.abs_path().is_empty() { return; } @@ -426,7 +426,7 @@ impl<'a> Scanner<'a> { return; } - let parts: [&[u8]; 2] = [entry.dir, entry.base()]; + let parts: [&[u8]; 2] = [entry.dir(), entry.base()]; // reshaped for borrowck — drop the &mut borrow from // abs_buf and reborrow open_dir_buf immutably so &self methods // below can be called with the slice. @@ -450,8 +450,8 @@ impl<'a> Scanner<'a> { Ok(s) => s, Err(_) => bun_core::out_of_memory(), }; - entry.abs_path = Interned::from_static(stored); - self.test_files.push(entry.abs_path); + entry.set_abs_path(Interned::from_static(stored)); + self.test_files.push(entry.abs_path()); } } } diff --git a/src/runtime/cli/test/parallel/Channel.rs b/src/runtime/cli/test/parallel/Channel.rs index 5b9584ad5b41..6f2131a446e2 100644 --- a/src/runtime/cli/test/parallel/Channel.rs +++ b/src/runtime/cli/test/parallel/Channel.rs @@ -21,6 +21,7 @@ use core::marker::PhantomData; use bun_collections::VecExt; use bun_jsc::virtual_machine::VirtualMachine; +use bun_ptr::ParentRef; use bun_sys::Fd; #[cfg(not(windows))] use bun_sys::FdExt as _; @@ -78,11 +79,13 @@ impl Default for Channel { } impl Channel { + /// Back-pointer to the `Owner` this channel is embedded in. #[inline] - fn owner(&mut self) -> &mut Owner { + fn owner_ref(&mut self) -> ParentRef { // SAFETY: `self` is always embedded at `Owner::OFFSET` inside an - // `Owner` that outlives all callbacks (see module doc). - unsafe { &mut *Owner::from_field_ptr(std::ptr::from_mut(self)) } + // `Owner` that outlives all callbacks (see module doc); `from_mut` + // keeps the write provenance `assume_mut` needs. + unsafe { ParentRef::from_raw_mut(Owner::from_field_ptr(std::ptr::from_mut(self))) } } } @@ -470,23 +473,16 @@ impl Channel { head += 5usize + len as usize; continue; }; - // borrowck split — `rd` borrows `self.r#in` while - // `owner()` would re-borrow `*self` mutably. Capture the owner raw - // pointer *before* forming `rd` (so the `&mut *self` reborrow ends - // immediately), then recover `&mut Owner` from it after. Same - // `container_of` arithmetic as `owner()`. The callback never - // touches `self.r#in` (it only reads `rd` and may write other - // channel fields / call `send()`), so the aliasing is sound. - // SAFETY: `self` is embedded at `Owner::OFFSET` inside an `Owner` - // that outlives all callbacks (see `Channel::owner()` / module doc). - let owner_ptr: *mut Owner = unsafe { Owner::from_field_ptr(std::ptr::from_mut(self)) }; + // borrowck split — take the back-pointer *before* `rd` borrows + // `self.r#in`, so the `&mut *self` reborrow ends immediately. The + // callback only reads `rd`; it never touches `self.r#in`. + let owner = self.owner_ref(); let mut rd = frame::Reader { p: &self.r#in[head + 5..][..len as usize], }; - // SAFETY: see `Channel::owner()` — `self` is embedded at - // `Owner::OFFSET` inside an `Owner` that outlives all callbacks. - let owner: &mut Owner = unsafe { &mut *owner_ptr }; - owner.on_channel_frame(kind, &mut rd); + // SAFETY: single-threaded loop callback; the owner outlives it and + // no other borrow of the owner is live. + unsafe { owner.assume_mut() }.on_channel_frame(kind, &mut rd); head += 5usize + len as usize; } self.r#in.drain_front(head); @@ -497,7 +493,10 @@ impl Channel { return; } self.done = true; - self.owner().on_channel_done(); + let owner = self.owner_ref(); + // SAFETY: single-threaded loop callback; the `&mut *self` reborrow ended + // above and no other borrow of the owner is live. + unsafe { owner.assume_mut() }.on_channel_done(); } } diff --git a/src/runtime/cli/test_command.rs b/src/runtime/cli/test_command.rs index 150537464980..2fce124695cf 100644 --- a/src/runtime/cli/test_command.rs +++ b/src/runtime/cli/test_command.rs @@ -804,8 +804,8 @@ impl CommandLineReporter { fn print_test_line( status: bun_test::Execution::Result, - sequence: &mut bun_test::Execution::ExecutionSequence, - test_entry: &mut bun_test::ExecutionEntry, + sequence: &bun_test::Execution::ExecutionSequence, + test_entry: &bun_test::ExecutionEntry, elapsed_ns: u64, writer: &mut impl bun_io::Write, ) { @@ -816,14 +816,14 @@ impl CommandLineReporter { let mut scopes_stack: BoundedArray<*const bun_test::DescribeScope, 64> = BoundedArray::default(); let mut parent_: Option<*const bun_test::DescribeScope> = - test_entry.base.parent.map(|p| p.cast_const()); + test_entry.base.parent.map(|p| p.as_ptr()); while let Some(scope) = parent_ { if scopes_stack.push(scope).is_err() { break; } // SAFETY: scope is a live DescribeScope pointer kept alive for the test run - parent_ = unsafe { (*scope).base.parent.map(|p| p.cast_const()) }; + parent_ = unsafe { (*scope).base.parent.map(|p| p.as_ptr()) }; } let scopes: &[*const bun_test::DescribeScope] = scopes_stack.as_slice(); @@ -1024,9 +1024,9 @@ impl CommandLineReporter { fn maybe_print_junit_line( status: bun_test::Execution::Result, - buntest: &mut bun_test::BunTest, - sequence: &mut bun_test::Execution::ExecutionSequence, - test_entry: &mut bun_test::ExecutionEntry, + buntest: &bun_test::BunTest, + sequence: &bun_test::Execution::ExecutionSequence, + test_entry: &bun_test::ExecutionEntry, elapsed_ns: u64, ) { let Some(cmd_reporter) = buntest.reporter else { @@ -1043,7 +1043,7 @@ impl CommandLineReporter { let mut scopes_stack: BoundedArray<*const bun_test::DescribeScope, 64> = BoundedArray::default(); let mut parent_: Option<*const bun_test::DescribeScope> = - test_entry.base.parent.map(|p| p.cast_const()); + test_entry.base.parent.map(|p| p.as_ptr()); let assertions = sequence.expect_call_count; let line_number = test_entry.base.line_no; @@ -1060,7 +1060,7 @@ impl CommandLineReporter { break; } // SAFETY: scope kept alive for the test run - parent_ = unsafe { (*scope).base.parent.map(|p| p.cast_const()) }; + parent_ = unsafe { (*scope).base.parent.map(|p| p.as_ptr()) }; } let scopes: &[*const bun_test::DescribeScope] = scopes_stack.as_slice(); @@ -1232,9 +1232,9 @@ impl CommandLineReporter { } pub fn handle_test_completed( - buntest: &mut bun_test::BunTest, - sequence: &mut bun_test::Execution::ExecutionSequence, - test_entry: &mut bun_test::ExecutionEntry, + buntest: &bun_test::BunTest, + sequence: &bun_test::Execution::ExecutionSequence, + test_entry: &bun_test::ExecutionEntry, elapsed_ns: u64, ) { let mut output_buf: Vec = Vec::new(); @@ -2002,12 +2002,10 @@ impl TestCommand { // `exec()` never returns before process exit, so the heap allocation // outlives all observers. - // `Loader::init` borrows the map; erase to `'static` via raw pointer round-trip + // `Loader::init` borrows the map; `Box::leak` gives it a `'static` borrow // (the map is never freed — process-lifetime singleton). - let env_map: *mut DotEnv::Map = bun_core::heap::into_raw(Box::new(DotEnv::Map::init())); - // SAFETY: `env_map` is heap-allocated and never freed; valid for process lifetime. - let mut env_loader: Box = - Box::new(DotEnv::Loader::init(unsafe { &mut *env_map })); + let env_map: &'static mut DotEnv::Map = Box::leak(Box::new(DotEnv::Map::init())); + let mut env_loader: Box = Box::new(DotEnv::Loader::init(env_map)); jsc::initialize(false); bun_http::http_thread::init(&Default::default()); diff --git a/src/runtime/cli/update_interactive_command.rs b/src/runtime/cli/update_interactive_command.rs index 9250567dacae..65f93d4cbf4a 100644 --- a/src/runtime/cli/update_interactive_command.rs +++ b/src/runtime/cli/update_interactive_command.rs @@ -2312,14 +2312,10 @@ fn update_default_catalog( // source == placement; otherwise re-`put` the mutated arena slot at the // placement-mandated location. let mut fresh_obj = E::Object::default(); - let (existing, source) = find_catalog_object(package_json, b"catalog"); + let (mut existing, source) = find_catalog_object(package_json, b"catalog"); { - let catalog_obj: &mut E::Object = match existing { - Some(mut o) => { - // SAFETY: `StoreRef` derefs into the live arena slot for the - // duration of this block; no other `&mut` to it is live. - unsafe { &mut *core::ptr::addr_of_mut!(*o) } - } + let catalog_obj: &mut E::Object = match existing.as_mut() { + Some(o) => &mut **o, None => &mut fresh_obj, }; @@ -2394,26 +2390,20 @@ fn update_named_catalog( // Reshaped — see `update_default_catalog` for the // shallow-copy-vs-in-place + lookup-vs-placement rationale. let mut fresh_catalogs = E::Object::default(); - let (existing_catalogs, source) = find_catalog_object(package_json, b"catalogs"); + let (mut existing_catalogs, source) = find_catalog_object(package_json, b"catalogs"); { - let catalogs_obj: &mut E::Object = match existing_catalogs { - Some(mut o) => { - // SAFETY: arena slot live for fn duration; no aliasing `&mut`. - unsafe { &mut *core::ptr::addr_of_mut!(*o) } - } + let catalogs_obj: &mut E::Object = match existing_catalogs.as_mut() { + Some(o) => &mut **o, None => &mut fresh_catalogs, }; // Get or create the specific catalog let mut fresh_catalog = E::Object::default(); - let existing_catalog: Option> = catalogs_obj + let mut existing_catalog: Option> = catalogs_obj .get(catalog_name) .and_then(|e| e.data.e_object()); - let catalog_obj: &mut E::Object = match existing_catalog { - Some(mut o) => { - // SAFETY: arena slot live for fn duration; no aliasing `&mut`. - unsafe { &mut *core::ptr::addr_of_mut!(*o) } - } + let catalog_obj: &mut E::Object = match existing_catalog.as_mut() { + Some(o) => &mut **o, None => &mut fresh_catalog, }; diff --git a/src/runtime/cli/upgrade_command.rs b/src/runtime/cli/upgrade_command.rs index 81d47e4ec1b1..8a6ac3446471 100644 --- a/src/runtime/cli/upgrade_command.rs +++ b/src/runtime/cli/upgrade_command.rs @@ -566,8 +566,8 @@ impl UpgradeCommand { fn _exec(ctx: Command::Context) -> Result<(), bun_core::Error> { HTTP::http_thread::init(&Default::default()); - // SAFETY: FileSystem::init returns the process-global singleton; valid for 'static. - let filesystem = unsafe { &mut *fs::FileSystem::init(None)? }; + fs::FileSystem::init(None)?; + let filesystem = fs::FileSystem::instance(); let mut env_loader: DotEnv::Loader = { // Allocate in the process-lifetime CLI arena. DotEnv::Loader::init(crate::cli::cli_arena().alloc(DotEnv::Map::init())) diff --git a/src/runtime/crypto/PBKDF2.rs b/src/runtime/crypto/PBKDF2.rs index 8ffc5aa6c83a..ca4a37dc3ff9 100644 --- a/src/runtime/crypto/PBKDF2.rs +++ b/src/runtime/crypto/PBKDF2.rs @@ -330,8 +330,8 @@ pub(crate) fn create_job(global_this: &JSGlobalObject, data: PBKDF2) -> *mut Job }, ) .expect("Pbkdf2Ctx::init is infallible"); - // SAFETY: `job` is a freshly-created live pointer. - unsafe { AnyTaskJob::schedule(job) }; + // SAFETY: `job` is a freshly-created, unscheduled, owned allocation. + AnyTaskJob::schedule(unsafe { bun_core::heap::take(job) }); job } diff --git a/src/runtime/crypto/PasswordObject.rs b/src/runtime/crypto/PasswordObject.rs index 950cc59d114f..d6a339e98eb7 100644 --- a/src/runtime/crypto/PasswordObject.rs +++ b/src/runtime/crypto/PasswordObject.rs @@ -632,22 +632,23 @@ struct PasswordResult { impl PasswordResult { fn run_from_js_erased(p: *mut Self) -> AnyTaskJsResult<()> { - Self::run_from_js(p) + // SAFETY: `p` was produced by heap::into_raw in `run_owned`; the event + // loop hands sole ownership to this callback. + unsafe { bun_core::heap::take(p) } + .run_from_js() .map_err(|_: jsc::JsTerminated| bun_event_loop::ErasedJsError::Terminated) } - fn run_from_js(this: *mut Self) -> Result<(), jsc::JsTerminated> { - // SAFETY: `this` was produced by heap::into_raw in `run_owned` and the - // event loop hands sole ownership to this callback. Reclaim the Box once - // up-front so all fields drop on scope exit (no `mem::replace` dance). - let this = *unsafe { bun_core::heap::take(this) }; + // `boxed_local`: the `Box` is the ownership unit being reclaimed here. + #[allow(clippy::boxed_local)] + fn run_from_js(self: Box) -> Result<(), jsc::JsTerminated> { let PasswordResult { value, mut r#ref, mut promise, global, task: _, - } = this; + } = *self; // SAFETY: `global` stored from a live `&JSGlobalObject`; VM outlives the task. let global = unsafe { &*global }; r#ref.unref(bun_io::js_vm_ctx()); diff --git a/src/runtime/dispatch.rs b/src/runtime/dispatch.rs index 22049be0b6ab..2d2babb6665e 100644 --- a/src/runtime/dispatch.rs +++ b/src/runtime/dispatch.rs @@ -233,17 +233,16 @@ pub fn run_task( let t = cast_ptr!($ty); // SAFETY: tag identifies pointee; heap-allocated at schedule time. let r = unsafe { (*t).run_from_js() }; - // SAFETY: paired with `create_on_js_thread` heap::alloc. - unsafe { <$ty>::destroy(t) }; + // SAFETY: paired with the `heap::into_raw` at each schedule site. + <$ty>::destroy(unsafe { bun_core::heap::take(t) }); r?; }}; (work $ty:ty) => {{ - let t = cast_ptr!($ty); - // SAFETY: tag identifies pointee; heap-allocated at schedule time. - let r = bun_jsc::work_task::WorkTask::run_from_js(unsafe { &mut *t }); - // SAFETY: paired with `create_on_js_thread` heap::alloc. - unsafe { bun_jsc::work_task::WorkTask::destroy(t) }; - r?; + // SAFETY: tag identifies pointee; paired with the `heap::into_raw` + // in `create_on_js_thread`. `run_from_js` destroys the task before + // it re-enters JS. + let t = unsafe { bun_core::heap::take(cast_ptr!($ty)) }; + bun_jsc::work_task::WorkTask::run_from_js(t)?; }}; } @@ -276,16 +275,24 @@ pub fn run_task( // `cast_ptr!` yields the heap-allocated task registered with this // tag; the JS-thread dispatch is the sole owner at this point. task_tag::ArchiveExtractTask => { - ArchiveAsyncTask::run_from_js(cast_ptr!(ArchiveExtractTask))?; + // SAFETY: §Dispatch — tag identifies pointee; sole owner at JS-thread dispatch. + let t = unsafe { bun_core::heap::take(cast_ptr!(ArchiveExtractTask)) }; + ArchiveAsyncTask::run_from_js(t)?; } task_tag::ArchiveBlobTask => { - ArchiveAsyncTask::run_from_js(cast_ptr!(ArchiveBlobTask))?; + // SAFETY: §Dispatch — tag identifies pointee; sole owner at JS-thread dispatch. + let t = unsafe { bun_core::heap::take(cast_ptr!(ArchiveBlobTask)) }; + ArchiveAsyncTask::run_from_js(t)?; } task_tag::ArchiveWriteTask => { - ArchiveAsyncTask::run_from_js(cast_ptr!(ArchiveWriteTask))?; + // SAFETY: §Dispatch — tag identifies pointee; sole owner at JS-thread dispatch. + let t = unsafe { bun_core::heap::take(cast_ptr!(ArchiveWriteTask)) }; + ArchiveAsyncTask::run_from_js(t)?; } task_tag::ArchiveFilesTask => { - ArchiveAsyncTask::run_from_js(cast_ptr!(ArchiveFilesTask))?; + // SAFETY: §Dispatch — tag identifies pointee; sole owner at JS-thread dispatch. + let t = unsafe { bun_core::heap::take(cast_ptr!(ArchiveFilesTask)) }; + ArchiveAsyncTask::run_from_js(t)?; } // ── shell interpreter (cold — hoisted to `run_task_cold`) ──────── @@ -313,7 +320,10 @@ pub fn run_task( // `cast_ptr!` yields the heap-allocated S3 task; JS-thread dispatch // is the sole owner here. task_tag::S3HttpSimpleTask => { - S3HttpSimpleTask::on_response(cast_ptr!(S3HttpSimpleTask))?; + // SAFETY: §Dispatch — tag identifies pointee; JS-thread dispatch is the sole + // owner (`AutoDeinit::ManualDeinit`), so ownership transfers into `on_response`. + let t = unsafe { bun_core::heap::take(cast_ptr!(S3HttpSimpleTask)) }; + S3HttpSimpleTask::on_response(t)?; } task_tag::S3HttpDownloadStreamingTask => { S3HttpDownloadStreamingTask::on_response(cast_ptr!(S3HttpDownloadStreamingTask)); @@ -355,12 +365,10 @@ pub fn run_task( // ── hot-reload (early-returns from the drain loop) ─────────────── task_tag::HotReloadTask => { - let t = cast_ptr!(hot_reloader::HotReloadTask); - // The task was heap-allocated in `Task::enqueue`; `deinit` frees it. - // SAFETY: tag identifies pointee; live Box'd HotReloadTask. - unsafe { (*t).run() }; - // SAFETY: paired with heap::alloc in `Task::enqueue`. - unsafe { hot_reloader::HotReloadTask::deinit(t) }; + // SAFETY: tag identifies pointee; paired with `heap::into_raw` in + // `Task::enqueue`. The box frees the task at the end of this arm. + let mut t = unsafe { bun_core::heap::take(cast_ptr!(hot_reloader::HotReloadTask)) }; + t.run(); return Ok(RunTaskResult::EarlyReturn); } // ── bake dev-server (cold — hoisted to `run_task_cold`) ────────── @@ -373,8 +381,9 @@ pub fn run_task( let t = cast_ptr!(FSWatchTask); // SAFETY: tag identifies pointee; live Box'd FSWatchTask. unsafe { (*t).run() }; - // SAFETY: paired with heap::alloc in `FSWatchTask::enqueue`. - unsafe { FSWatchTask::deinit(t) }; + // SAFETY: paired with heap::alloc in `FSWatchTask::enqueue`; `t` is + // the unique live pointer and is not used after this. + FSWatchTask::deinit(unsafe { bun_core::heap::take(t) }); } // ── DNS ────────────────────────────────────────────────────────── @@ -529,19 +538,24 @@ fn run_task_cold(task: Task) { match task.tag { // ── shell interpreter ──────────────────────────────────────────── task_tag::ShellAsync => { - // SAFETY: §Dispatch — tag identifies pointee. - let t = unsafe { &mut *cast_ptr!(crate::shell::dispatch_tasks::ShellAsyncTask) }; - // SAFETY: `interp` set at enqueue; outlives task. - let interp = unsafe { &*t.interp }; - ShellAsync::run_from_main_thread(interp, t.node); + let t = cast_ptr!(crate::shell::dispatch_tasks::ShellAsyncTask); + // SAFETY: §Dispatch — tag identifies pointee; `interp` set at enqueue + // and outlives the task. Raw reads only: no borrow of `*t` spans the + // JS re-entry inside `run_from_main_thread`. + let (interp, node) = unsafe { (&*(*t).interp, (*t).node) }; + ShellAsync::run_from_main_thread(interp, node); } task_tag::ShellAsyncSubprocessDone => { let t = cast_ptr!(ShellAsyncSubprocessDone); - ShellAsyncSubprocessDone::run_from_main_thread(t); + // SAFETY: §Dispatch — tag identifies pointee; paired with the + // `heap::alloc` in `ShellSubprocess::on_process_exit`. + ShellAsyncSubprocessDone::run_from_main_thread(unsafe { bun_core::heap::take(t) }); } task_tag::ShellIOWriterAsyncDeinit => { let t = cast_ptr!(ShellIOWriterAsyncDeinit); - ShellIOWriterAsyncDeinit::run_from_main_thread(t); + // SAFETY: §Dispatch — tag identifies pointee; `t` is the unique + // owning pointer to the `heap::alloc` payload. + ShellIOWriterAsyncDeinit::run_from_main_thread(unsafe { bun_core::heap::take(t) }); } task_tag::ShellIOWriter => { let t = cast_ptr!(ShellIOWriter); @@ -549,7 +563,9 @@ fn run_task_cold(task: Task) { } task_tag::ShellIOReaderAsyncDeinit => { let t = cast_ptr!(ShellIOReaderAsyncDeinit); - ShellIOReaderAsyncDeinit::run_from_main_thread(t); + // SAFETY: §Dispatch — tag identifies pointee; the payload is the + // `heap::alloc` box enqueued by `IOReader::async_deinit`. + ShellIOReaderAsyncDeinit::run_from_main_thread(unsafe { bun_core::heap::take(t) }); } task_tag::ShellCondExprStatTask => { shell_dispatch!(nested ShellCondExprStatTask); @@ -644,13 +660,6 @@ pub unsafe fn __bun_run_file_poll(poll: *mut FilePoll, size_or_offset: i64) { debug_assert!(!owner.is_null()); - /// `ptr.as(T)` — recover the typed owner. - macro_rules! owner_as { - ($ty:ty) => {{ - // SAFETY: tag set with this pointee type at `FilePoll::init`. - unsafe { &mut *owner.ptr.cast::<$ty>() } - }}; - } /// One match-arm body of the poll-tag dispatch. Recovers the typed owner as /// a RAW `*mut $Ty` (never `&mut` — re-entrant callees like `DNSResolver` /// pick their own deref mode without aliasing UB) then runs `$body`. The @@ -689,10 +698,18 @@ pub unsafe fn __bun_run_file_poll(poll: *mut FilePoll, size_or_offset: i64) { crate::node::memory_pressure::on_poll(unsafe { &mut *poll }, size_or_offset); } poll_tag::PARENT_DEATH_WATCHDOG => { - let wd = owner_as!(bun_io::parent_death_watchdog::ParentDeathWatchdog); + // SAFETY: tag set with this pointee type at `FilePoll::init`; the + // watchdog is process-global and outlives every poll. + let wd = unsafe { + bun_ptr::BackRef::from_raw( + owner + .ptr + .cast::(), + ) + }; // Mac-only — debug-assert elsewhere (Linux uses prctl(PR_SET_PDEATHSIG)). #[cfg(target_os = "macos")] - bun_io::parent_death_watchdog::on_parent_exit(wd); + bun_io::parent_death_watchdog::on_parent_exit(wd.get()); #[cfg(not(target_os = "macos"))] { debug_assert!(false, "ParentDeathWatchdog poll on non-mac"); @@ -1189,7 +1206,11 @@ pub(crate) fn __bun_release_task_at_shutdown(task: bun_event_loop::Task) -> bool // `AsyncFSTask::create`. The work-pool callback ran // (it posted this entry) so the threadpool no longer // holds the embedded `task` field. - unsafe { fs_async::$ty::destroy(task.ptr.cast::()) }; + unsafe { + fs_async::$ty::destroy(bun_core::heap::take( + task.ptr.cast::(), + )) + }; })* // SAFETY: outer arm guard proves one of the table tags matched. _ => unsafe { core::hint::unreachable_unchecked() }, diff --git a/src/runtime/dns_jsc/dns.rs b/src/runtime/dns_jsc/dns.rs index bc593dc53905..d6017fa9d52a 100644 --- a/src/runtime/dns_jsc/dns.rs +++ b/src/runtime/dns_jsc/dns.rs @@ -2128,9 +2128,9 @@ impl GlobalData { impl Drop for GlobalData { fn drop(&mut self) { - // `Resolver::deinit` ends with `heap::take(this)`, which is wrong for a - // value field — open-code the channel teardown so the c-ares state - // frees when this box drops in `deinit_runtime_state`. + // `Resolver`'s refcount destructor frees the heap allocation, which is + // wrong for a value field — open-code the channel teardown so the + // c-ares state frees when this box drops in `deinit_runtime_state`. if let Some(channel) = self.resolver.channel.take() { // SAFETY: `channel` is the live handle from `ares_init_options`, owned by this resolver. unsafe { c_ares::Channel::destroy(channel) }; @@ -2336,20 +2336,14 @@ pub mod internal { false } - /// # Safety - /// `this` must be the heap-allocated `Request` returned by `Request::new` - /// with `refcount == 0`; freed by this call. - // `this` is reclaimed via `heap::take` (Box::from_raw); forming - // `&mut *this` at entry would invalidate the pointer's allocation - // provenance, so the param must stay `*mut`. - #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub fn deinit(this: *mut Self) { - // SAFETY: this is a heap-allocated Request with refcount==0 - unsafe { - debug_assert!((*this).notify.is_empty()); - // `result_buf` (Box<[ResultEntry]>) and `key.host` freed by Drop. - drop(bun_core::heap::take(this)); - } + /// Callers must have unlinked the entry from the cache and observed + /// `refcount == 0` before taking ownership. + // `boxed_local`: the `Box` is the ownership unit being reclaimed here. + #[allow(clippy::boxed_local)] + pub fn deinit(this: Box) { + debug_assert!(this.notify.is_empty()); + // `result_buf` (Box<[ResultEntry]>) and `key.host` free here, on Drop. + drop(this); } } @@ -2392,7 +2386,7 @@ pub mod internal { bun_output::scoped_log!(dns, "get: expired entry"); if (*entry).refcount == 0 { let _ = self.delete_entry_at(len, i); - Request::deinit(entry); + Request::deinit(bun_core::heap::take(entry)); len = self.len; } continue; @@ -2449,7 +2443,7 @@ pub mod internal { // SAFETY: entries are valid unsafe { if (**e).refcount == 0 { - Request::deinit(*e); + Request::deinit(bun_core::heap::take(*e)); *e = entry; return true; } @@ -3250,7 +3244,7 @@ pub mod internal { if (*req).refcount == 0 && (guard.is_nearly_full() || !(*req).valid) { bun_output::scoped_log!(dns, "cache --"); guard.remove(req); - Request::deinit(req); + Request::deinit(bun_core::heap::take(req)); } } } @@ -3699,7 +3693,9 @@ impl bun_ptr::RefCounted for Resolver { unsafe { &raw mut (*this).ref_count } } unsafe fn destructor(this: *mut Self, _ctx: ()) { - Self::deinit(this); + // SAFETY: the refcount hit zero, so `this` is the sole live pointer to + // the `heap::into_raw` allocation from `init`. + Self::deinit(unsafe { bun_core::heap::take(this) }); } } @@ -4046,14 +4042,10 @@ impl Resolver { unsafe { Self::deref(Box::into_raw(self)) }; } - fn deinit(this: *mut Self) { - // SAFETY: `this` is the heap allocation from `init()`; refcount has hit - // zero (sole caller is `Self::deref`), so we hold exclusive ownership. - unsafe { - if let Some(channel) = (*this).channel.get() { - c_ares::Channel::destroy(channel); - } - drop(bun_core::heap::take(this)); + fn deinit(self: Box) { + if let Some(channel) = self.channel.get() { + // SAFETY: `channel` is the live handle from `ares_init_options`, owned by this resolver. + unsafe { c_ares::Channel::destroy(channel) }; } } @@ -4211,22 +4203,14 @@ impl Resolver { /// Dispatch to the GetAddrInfo PendingCache by field enum. /// - /// R-2: returns `&mut` from `&self` via `JsCell::get_mut`. Callers hold - /// the borrow only for the duration of a slot read/claim/unset and never - /// across a re-entrant call (the c-ares callback path that re-enters the - /// resolver runs *after* the borrow is dropped). - #[allow(clippy::mut_from_ref)] - fn pending_host_cache(&self, field: PendingCacheField) -> &mut PendingCache { - // SAFETY: single-JS-thread invariant; caller holds the borrow only for - // a short, non-reentrant window (see fn doc). - unsafe { - match field { - PendingCacheField::PendingHostCacheCares => self.pending_host_cache_cares.get_mut(), - PendingCacheField::PendingHostCacheNative => { - self.pending_host_cache_native.get_mut() - } - _ => unreachable!(), - } + /// Every `HiveArray` slot op takes `&self` (buffer is `UnsafeCell`, `used` is + /// `Cell`-backed), so slot pointers handed out as `CacheHit` keep their write + /// permission across the re-entrant c-ares/JS callback path. + fn pending_host_cache(&self, field: PendingCacheField) -> &PendingCache { + match field { + PendingCacheField::PendingHostCacheCares => self.pending_host_cache_cares.get(), + PendingCacheField::PendingHostCacheNative => self.pending_host_cache_native.get(), + _ => unreachable!(), } } @@ -4658,12 +4642,12 @@ impl Resolver { while let Some(index) = inflight_iter.next() { // SAFETY: `used` bit is set ⇒ slot was initialized. - let entry = unsafe { &mut *cache.ptr_at(index) }; + let entry = unsafe { &*cache.ptr_at(index) }; if R::key_hash(entry) == R::key_hash(key) && R::key_len(entry) == R::key_len(key) && R::key_name(entry) == R::key_name(key) { - return LookupCacheHit::Inflight(std::ptr::from_mut(entry)); + return LookupCacheHit::Inflight(cache.ptr_at(index)); } } @@ -4684,9 +4668,9 @@ impl Resolver { while let Some(index) = inflight_iter.next() { // SAFETY: `used` bit is set ⇒ slot was initialized. - let entry = unsafe { &mut *cache.ptr_at(index) }; + let entry = unsafe { &*cache.ptr_at(index) }; if entry.hash == key.hash && entry.len == key.len && entry.name == key.name { - return CacheHit::Inflight(std::ptr::from_mut(entry)); + return CacheHit::Inflight(cache.ptr_at(index)); } } @@ -4900,8 +4884,7 @@ impl Resolver { Async::posix_event_loop::poll_tag::DNS_RESOLVER, self.as_ctx_ptr().cast::<()>(), ); - // SAFETY: `event_loop_handle` is set once VM is initialized; live for VM lifetime. - let loop_ = unsafe { &mut *self.vm().event_loop_handle.unwrap() }; + let loop_ = self.vm().platform_loop_opt().expect("event_loop_handle"); // SAFETY: single-JS-thread; the `&mut PollsMap` borrow does not span // any re-entrant call (`FilePoll::register` is a syscall wrapper). let polls = unsafe { self.polls.get_mut() }; diff --git a/src/runtime/ffi/ffi_body.rs b/src/runtime/ffi/ffi_body.rs index d3645a35ea7a..df41c132fc3e 100644 --- a/src/runtime/ffi/ffi_body.rs +++ b/src/runtime/ffi/ffi_body.rs @@ -439,53 +439,65 @@ static CACHED_DEFAULT_SYSTEM_LIBRARY_DIR: OnceLock = OnceLock::n #[cfg(any(target_os = "macos", target_os = "linux", target_os = "android"))] static CACHED_DEFAULT_SYSTEM_INCLUDE_DIR_ONCE: Once = Once::new(); -impl CompileC { - /// # Safety - /// `this_` is the `ConfigErr::ctx` pointer round-tripped through TinyCC; it - /// must be null or point to a live `CompileC`. `message` is a NUL-terminated - /// C string when non-null. Signature matches `ConfigErr::handler` exactly so - /// it can be passed without an ABI-coercing cast. - pub(crate) unsafe extern "C" fn handle_compilation_error( - this_: *mut CompileC, - message: *const c_char, - ) { - if this_.is_null() { - return; +/// Sink for TinyCC's error callback, invoked by `tcc_error_trampoline` once it +/// has done the single raw-pointer deref. `msg` is the raw TCC message. +pub(crate) trait TccErrorSink { + fn on_tcc_error(&mut self, msg: &[u8]); +} + +/// The one deref site for a `ConfigErr::ctx` round-tripped through TinyCC. +/// +/// # Safety +/// `ctx` must be null or point to a live `T`; `message` is a NUL-terminated C +/// string when non-null. Signature matches `ConfigErr::handler` exactly. +pub(crate) unsafe extern "C" fn tcc_error_trampoline( + ctx: *mut T, + message: *const c_char, +) { + // SAFETY: per the fn contract, `ctx` is null or the live `T` we gave TinyCC. + let Some(this) = (unsafe { ctx.as_mut() }) else { + return; + }; + let msg: &[u8] = if message.is_null() { + b"" + } else { + // SAFETY: TCC guarantees `message` is a valid NUL-terminated string when non-null. + unsafe { bun_core::ffi::cstr(message) }.to_bytes() + }; + this.on_tcc_error(msg); +} + +/// the message we get from TCC sometimes has garbage in it +/// i think because we're doing in-memory compilation +fn trim_tcc_message(msg: &[u8]) -> &[u8] { + let mut offset: usize = 0; + while offset < msg.len() { + if msg[offset] > 0x20 && msg[offset] < 0x7f { + break; } - // SAFETY: TinyCC threads our own `&mut CompileC` back as `ctx`; we hold - // the unique borrow for the duration of the callback. - let this = unsafe { &mut *this_ }; - let mut msg: &[u8] = if message.is_null() { - b"" - } else { - // SAFETY: TCC guarantees `message` is a valid NUL-terminated string when non-null. - unsafe { bun_core::ffi::cstr(message) }.to_bytes() - }; + offset += 1; + } + &msg[offset..] +} + +impl TccErrorSink for CompileC { + fn on_tcc_error(&mut self, msg: &[u8]) { if msg.is_empty() { return; } - - let mut offset: usize = 0; - // the message we get from TCC sometimes has garbage in it - // i think because we're doing in-memory compilation - while offset < msg.len() { - if msg[offset] > 0x20 && msg[offset] < 0x7f { - break; - } - offset += 1; - } - msg = &msg[offset..]; - - this.deferred_errors.push(Box::<[u8]>::from(msg)); + self.deferred_errors + .push(Box::<[u8]>::from(trim_tcc_message(msg))); } +} +impl CompileC { #[inline] fn has_deferred_errors(&self) -> bool { !self.deferred_errors.is_empty() } /// Returns DeferredError if any errors from tinycc were registered - /// via `handle_compilation_error` + /// via `tcc_error_trampoline` #[inline] fn error_check(&self) -> Result<(), DeferredError> { if !self.deferred_errors.is_empty() { @@ -621,7 +633,7 @@ impl CompileC { output_type: TCC::OutputFormat::Memory, err: TCC::ConfigErr { ctx: Some(std::ptr::from_mut::(self)), - handler: Self::handle_compilation_error, + handler: tcc_error_trampoline::, }, }) { Ok(s) => s, @@ -1892,7 +1904,7 @@ pub(super) fn generate_symbols( pub struct Function { pub symbol_from_dynamic_library: Option<*mut c_void>, pub base_name: Option, - pub state: Option>, + pub state: Cell>>, pub return_type: ABIType, pub arg_types: Vec, @@ -1906,7 +1918,7 @@ impl Default for Function { Self { symbol_from_dynamic_library: None, base_name: None, - state: None, + state: Cell::new(None), return_type: ABIType::Void, arg_types: Vec::new(), step: Step::Pending, @@ -1935,6 +1947,15 @@ impl Drop for Function { } } +impl TccErrorSink for Function { + fn on_tcc_error(&mut self, msg: &[u8]) { + self.step = Step::Failed { + msg: Box::<[u8]>::from(trim_tcc_message(msg)), + allocated: true, + }; + } +} + impl Function { pub(crate) fn needs_handle_scope(&self) -> bool { for arg in self.arg_types.iter() { @@ -1958,36 +1979,6 @@ impl Function { bun_core::runtime_embed_file!(Src, "runtime/ffi/FFI.h").as_bytes() } - /// # Safety - /// `ctx` is the `ConfigErr::ctx` pointer round-tripped through TinyCC and - /// must point to a live `Function`. `message` is a NUL-terminated C string. - /// Signature matches `ConfigErr::handler` exactly so it can be passed - /// without an ABI-coercing cast. - pub(crate) unsafe extern "C" fn handle_tcc_error(ctx: *mut Function, message: *const c_char) { - debug_assert!(!ctx.is_null()); - // SAFETY: TinyCC threads our own `&mut Function` back as `ctx`. - let this = unsafe { &mut *ctx }; - // SAFETY: TCC passes a valid NUL-terminated string - let mut msg: &[u8] = unsafe { bun_core::ffi::cstr(message) }.to_bytes(); - if !msg.is_empty() { - let mut offset: usize = 0; - // the message we get from TCC sometimes has garbage in it - // i think because we're doing in-memory compilation - while offset < msg.len() { - if msg[offset] > 0x20 && msg[offset] < 0x7f { - break; - } - offset += 1; - } - msg = &msg[offset..]; - } - - this.step = Step::Failed { - msg: Box::<[u8]>::from(msg), - allocated: true, - }; - } - pub(crate) fn compile( &mut self, napi_env: Option<&napi::NapiEnv>, @@ -2001,31 +1992,26 @@ impl Function { } else { zstr!("-std=c11 -nostdlib -Wl,--export-all-symbols") }; - let state = match TCC::State::init::(&TCC::Config { + let state_ptr = match TCC::State::init::(&TCC::Config { options: Some(NonNull::from(tcc_options)), output_type: TCC::OutputFormat::Memory, err: TCC::ConfigErr { ctx: Some(std::ptr::from_mut::(self)), - handler: Self::handle_tcc_error, + handler: tcc_error_trampoline::, }, }) { Ok(s) => s, Err(_) => return Err(bun_core::err!("TCCMissing")), }; - self.state = Some(state); - let _guard = scopeguard::guard(std::ptr::from_mut::(self), |this_ptr| { - // SAFETY: this_ptr is &mut self for the duration of compile() - let this = unsafe { &mut *this_ptr }; - if matches!(this.step, Step::Failed { .. }) { - if let Some(s) = this.state.take() { - // SAFETY: we own the state - unsafe { TCC::State::destroy(s.as_ptr()) }; - } - } + // The guard owns the state until the `Compiled` step defuses it; every + // early return below therefore leaves `self.state` unset. + let state_guard = scopeguard::guard(state_ptr, |s| { + // SAFETY: we own the state + unsafe { TCC::State::destroy(s.as_ptr()) }; }); - // SAFETY: state is non-null, just stored above - let state = unsafe { self.state.unwrap().as_mut() }; + // SAFETY: `state_ptr` was just returned non-null by `TCC::State::init`. + let state: &mut TCC::State = unsafe { &mut *state_ptr.as_ptr() }; if let Some(env) = napi_env { // `env` is the live VM-owned napi env; process-lifetime. @@ -2077,6 +2063,8 @@ impl Function { return Ok(()); }; + self.state + .set(Some(scopeguard::ScopeGuard::into_inner(state_guard))); self.step = Step::Compiled(Compiled { ptr: symbol.as_ptr().cast::(), ..Default::default() @@ -2122,12 +2110,12 @@ impl Function { } else { zstr!("-std=c11 -nostdlib -Wl,--export-all-symbols") }; - let state = match TCC::State::init::(&TCC::Config { + let state_ptr = match TCC::State::init::(&TCC::Config { options: Some(NonNull::from(tcc_options)), output_type: TCC::OutputFormat::Memory, err: TCC::ConfigErr { ctx: Some(std::ptr::from_mut::(self)), - handler: Self::handle_tcc_error, + handler: tcc_error_trampoline::, }, }) { Ok(s) => s, @@ -2140,19 +2128,14 @@ impl Function { // aren't possible Err(_) => unreachable!(), }; - self.state = Some(state); - let _guard = scopeguard::guard(std::ptr::from_mut::(self), |this_ptr| { - // SAFETY: this_ptr is &mut self for the duration of compile_callback() - let this = unsafe { &mut *this_ptr }; - if matches!(this.step, Step::Failed { .. }) { - if let Some(s) = this.state.take() { - // SAFETY: we own the state - unsafe { TCC::State::destroy(s.as_ptr()) }; - } - } + // The guard owns the state until the `Compiled` step defuses it; every + // early return below therefore leaves `self.state` unset. + let state_guard = scopeguard::guard(state_ptr, |s| { + // SAFETY: we own the state + unsafe { TCC::State::destroy(s.as_ptr()) }; }); - // SAFETY: just stored above - let state = unsafe { self.state.unwrap().as_mut() }; + // SAFETY: `state_ptr` was just returned non-null by `TCC::State::init`. + let state: &mut TCC::State = unsafe { &mut *state_ptr.as_ptr() }; if self.needs_napi_env() { if state @@ -2215,6 +2198,8 @@ impl Function { return Ok(()); }; + self.state + .set(Some(scopeguard::ScopeGuard::into_inner(state_guard))); self.step = Step::Compiled(Compiled { ptr: symbol.as_ptr().cast::(), // SAFETY: opaque-handle storage only. Never diff --git a/src/runtime/hw_exports.rs b/src/runtime/hw_exports.rs index dd343b1a658c..5d32bf559957 100644 --- a/src/runtime/hw_exports.rs +++ b/src/runtime/hw_exports.rs @@ -211,10 +211,13 @@ pub(crate) mod sql_hooks { opts: &bun_uws::us_bun_socket_context_options_t, err: &mut bun_uws::create_bun_socket_error_t, ) -> *mut bun_uws::SslCtx { - // SAFETY: `cache` is `&runtime_state().ssl_ctx_cache`. - let cache = unsafe { &mut *cache.cast::() }; + // SAFETY: `cache` points at `runtime_state().ssl_ctx_cache`, live for + // the VM; the shared borrow scopes mutation inside `with_mut`. + let cache = unsafe { + &*cache.cast::>() + }; cache - .get_or_create_opts(opts, err) + .with_mut(|c| c.get_or_create_opts(opts, err)) .unwrap_or(core::ptr::null_mut()) } unsafe fn ssl_config_from_js(global: &JSGlobalObject, value: JSValue) -> *mut c_void { diff --git a/src/runtime/image/Image.rs b/src/runtime/image/Image.rs index 35a391404382..f8ff70938e7a 100644 --- a/src/runtime/image/Image.rs +++ b/src/runtime/image/Image.rs @@ -1257,12 +1257,10 @@ impl<'a> BlobReadChain<'a> { unreachable!() }; let blob_js = strong.get(); - let Some(blob) = blob_js.as_::() else { + let Some(blob) = blob_js.as_class_ref::() else { drop(deliver); return Err(global.throw(format_args!("Image: Blob source is no longer a Blob"))); }; - // SAFETY: `as_` returned a non-null `*mut Blob` rooted by `blob_js`. - let blob = unsafe { &mut *blob }; // Same Strong-ref contract as the regular pending_tasks bump — keeps // the wrapper (and its sourceJS slot) alive until the read settles. diff --git a/src/runtime/ipc_host.rs b/src/runtime/ipc_host.rs index 27d42668437d..a01d7514dee2 100644 --- a/src/runtime/ipc_host.rs +++ b/src/runtime/ipc_host.rs @@ -144,10 +144,9 @@ pub(crate) fn do_send( match unsafe { (*listener).listener.get() } { crate::socket::listener::ListenerType::Uws(socket_uws) => { // may need to handle ssl case - // SAFETY: `socket_uws` is a live non-null `*mut ListenSocket` - // owned by uSockets; `get_socket` only reinterpret-casts to - // `&mut us_socket_t` and `get_fd` is a read-only FFI call. - let fd = unsafe { &mut *socket_uws }.get_socket().get_fd(); + let fd = bun_opaque::opaque_deref_mut(socket_uws) + .get_socket() + .get_fd(); zig_handle = Some(Handle::init(fd, handle)); } crate::socket::listener::ListenerType::NamedPipe(_named_pipe) => {} diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index 858321dbda5c..8c28462f487e 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -75,7 +75,7 @@ pub struct RuntimeState { pub sql_rare: bun_sql_jsc::jsc::RareData, /// `RareData.ssl_ctx_cache` — concrete digest-keyed weak `SSL_CTX*` cache. /// Same cycle-break story as `sql_rare`. - pub ssl_ctx_cache: crate::api::SSLContextCache::SSLContextCache, + pub ssl_ctx_cache: bun_jsc::JsCell, /// `RareData.editor_context` — `bun_jsc` cannot name `crate::cli::open`. pub editor_context: crate::cli::open::EditorContext, /// `RareData.global_dns_data` — per-VM resolver + c-ares channel. @@ -234,7 +234,7 @@ pub(crate) unsafe fn default_client_ssl_ctx(vm: *mut VirtualMachine) -> *mut bun ); // SAFETY: per-thread `RuntimeState`; `ssl_ctx_cache` has a stable // address for the VM's lifetime and is only touched from the JS thread. - let cache = unsafe { &mut (*state).ssl_ctx_cache }; + let cache = unsafe { &(*state).ssl_ctx_cache }; // Mode-neutral CTX (VERIFY_NONE). `us_internal_ssl_attach` overrides // each client SSL to VERIFY_PEER + the shared bundled-root store, so // `new WebSocket("wss://…")` (which shares this CTX and defaults to @@ -243,7 +243,7 @@ pub(crate) unsafe fn default_client_ssl_ctx(vm: *mut VirtualMachine) -> *mut bun // to the same CTX rather than building a second one with the same // digest. The +1 ref returned here is held for the VM's lifetime, so // the entry never tombstones. - match cache.get_or_create_opts(&Default::default(), &mut err) { + match cache.with_mut(|c| c.get_or_create_opts(&Default::default(), &mut err)) { Some(ctx) => rare.default_client_ssl_ctx = Some(ctx), None => bun_core::Output::panic(format_args!( "default client SSL_CTX init failed: {}", @@ -272,8 +272,8 @@ unsafe fn ssl_ctx_cache_get_or_create( ); // SAFETY: per-thread `RuntimeState`; `ssl_ctx_cache` has a stable // address for the VM's lifetime and is only touched from the JS thread. - let cache = unsafe { &mut (*state).ssl_ctx_cache }; - cache.get_or_create_opts(opts, err) + let cache = unsafe { &(*state).ssl_ctx_cache }; + cache.with_mut(|c| c.get_or_create_opts(opts, err)) } // ════════════════════════════════════════════════════════════════════════════ @@ -506,7 +506,7 @@ unsafe fn configure_debugger( Some(Debugger { path_or_port: None, from_environment_variable: unix, - wait_for_connection, + wait_for_connection: Cell::new(wait_for_connection), set_breakpoint_on_first_line, ..Default::default() }) @@ -514,7 +514,7 @@ unsafe fn configure_debugger( Some(Debugger { path_or_port: None, from_environment_variable: connect_to, - wait_for_connection: Wait::Off, + wait_for_connection: Cell::new(Wait::Off), set_breakpoint_on_first_line: false, mode: Mode::Connect, ..Default::default() @@ -530,11 +530,11 @@ unsafe fn configure_debugger( Some(Debugger { path_or_port: Some(path_or_port), from_environment_variable: unix, - wait_for_connection: if enable.wait_for_connection { + wait_for_connection: Cell::new(if enable.wait_for_connection { Wait::Forever } else { wait_for_connection - }, + }), set_breakpoint_on_first_line: set_breakpoint_on_first_line || enable.set_breakpoint_on_first_line, ..Default::default() @@ -1614,8 +1614,9 @@ unsafe fn retroactively_report_discovered_tests(agent: *mut bun_jsc::debugger::T let mut max_id: i32 = 0; // Recursively report all discovered tests starting from root scope. + // SAFETY: `agent` is a live C++ handle (fn contract). retroactively_report_scope( - agent, + unsafe { &mut *agent }, &mut active_file.collection.root_scope, -1, &mut max_id, @@ -1627,7 +1628,7 @@ unsafe fn retroactively_report_discovered_tests(agent: *mut bun_jsc::debugger::T let _ = max_id; fn retroactively_report_scope( - agent: *mut TestReporterHandle, + agent: &mut TestReporterHandle, scope: &mut DescribeScope, parent_id: i32, max_id: &mut i32, @@ -1645,8 +1646,7 @@ unsafe fn retroactively_report_discovered_tests(agent: *mut bun_jsc::debugger::T let mut name = bun_core::String::init( describe.base.name.as_deref().unwrap_or(b"(unnamed)"), ); - // SAFETY: `agent` is a live C++ handle (fn contract). - unsafe { &mut *agent }.report_test_found_with_location( + agent.report_test_found_with_location( test_id, &mut name, TestType::Describe, @@ -1672,8 +1672,7 @@ unsafe fn retroactively_report_discovered_tests(agent: *mut bun_jsc::debugger::T let mut name = bun_core::String::init( test_entry.base.name.as_deref().unwrap_or(b"(unnamed)"), ); - // SAFETY: `agent` is a live C++ handle (fn contract). - unsafe { &mut *agent }.report_test_found_with_location( + agent.report_test_found_with_location( test_id, &mut name, TestType::Test, @@ -2252,39 +2251,29 @@ fn transpile_source_code_inner( _ => ModuleType::Unknown, }; - let mut input_file_fd = bun_sys::Fd::INVALID; + let input_file_fd = Cell::new(bun_sys::Fd::INVALID); // The deferred fd close is independent of `give_back_arena` // and must fire on every exit path: parse failure, JSON early // return, `disable_transpilying`, already_bundled, empty `.cjs`, // cache-hit, AsyncModule, the wasm recurse, and the print error. - // Note: reshaped for borrowck — capture raw pointers so the - // guard does not alias the parser's `file_fd_ptr` / - // `maybe_watch_file` borrows. **All** later access to - // `should_close_input_file_fd` / `input_file_fd` MUST go through - // these raw pointers — taking a fresh `&mut` to either local would - // invalidate the guard's tag under Stacked Borrows, making the - // deferred `.close()` (which the parse path always reaches) UB. + // Note: reshaped for borrowck — the `should_close` raw pointer + // keeps the guard from aliasing `maybe_watch_file`'s `&mut` borrow; + // the fd is a `Cell`, so a shared borrow suffices for both. let should_close_ptr: *mut bool = &raw mut should_close_input_file_fd; - let input_file_fd_ptr: *mut bun_sys::Fd = &raw mut input_file_fd; - // Note: `scopeguard::defer!` would capture the two `*mut` - // locals by-ref in its non-`move` closure, which borrowck then - // treats as conflicting with the later `&mut *ptr` reborrows below - // (edition-2021 capture analysis). Thread the raw pointers through - // the guard *payload* instead so nothing is captured. + // Note: `scopeguard::defer!` would capture the locals by-ref in its + // non-`move` closure, which borrowck then treats as conflicting + // with the later reborrows. Thread them through the guard *payload* + // instead so nothing is captured. let _fd_guard = scopeguard::guard( - (should_close_ptr, input_file_fd_ptr), - |(should_close_ptr, input_file_fd_ptr)| { - // SAFETY: `should_close_input_file_fd` / `input_file_fd` - // are declared earlier in this stack frame and outlive - // this guard (locals drop in reverse declaration order); - // the guard runs on the same thread before either is - // destroyed. - unsafe { - if *should_close_ptr && (*input_file_fd_ptr).is_valid() { - use bun_sys::FdExt as _; - (*input_file_fd_ptr).close(); - *input_file_fd_ptr = bun_sys::Fd::INVALID; - } + (should_close_ptr, &input_file_fd), + |(should_close_ptr, input_file_fd)| { + // SAFETY: `should_close_input_file_fd` is declared earlier + // in this stack frame and outlives this guard; the guard + // runs on the same thread before it is destroyed. + if unsafe { *should_close_ptr } && input_file_fd.get().is_valid() { + use bun_sys::FdExt as _; + input_file_fd.get().close(); + input_file_fd.set(bun_sys::Fd::INVALID); } }, ); @@ -2404,11 +2393,7 @@ fn transpile_source_code_inner( loader, dirname_fd: bun_sys::Fd::INVALID, file_descriptor: fd, - // SAFETY: `input_file_fd_ptr` points at this frame's - // `input_file_fd`; reborrow through the raw pointer so the - // `_fd_guard` scopeguard's tag is not invalidated by a - // fresh `&mut` (see Note on `_fd_guard`). - file_fd_ptr: Some(unsafe { &mut *input_file_fd_ptr }), + file_fd_ptr: Some(&input_file_fd), file_hash: Some(hash), macro_remappings, // SAFETY: per fn contract — `jsc_vm` is the live per-thread VM. @@ -2471,12 +2456,10 @@ fn transpile_source_code_inner( let Some(mut parse_result) = parse_result else { // Register with watcher even on parse failure. if !disable_transpilying { - // SAFETY: see Note on `_fd_guard` — reborrow via - // the raw pointers so the guard stays valid. maybe_watch_file( jsc_vm, - unsafe { &mut *should_close_ptr }, - unsafe { *input_file_fd_ptr }, + &mut should_close_input_file_fd, + input_file_fd.get(), is_node_override, path, hash, @@ -2520,12 +2503,10 @@ fn transpile_source_code_inner( // Register with watcher on success too. if !disable_transpilying { - // SAFETY: see Note on `_fd_guard` — reborrow via the - // raw pointers so the guard stays valid. maybe_watch_file( jsc_vm, - unsafe { &mut *should_close_ptr }, - unsafe { *input_file_fd_ptr }, + &mut should_close_input_file_fd, + input_file_fd.get(), is_node_override, path, hash, @@ -2881,13 +2862,6 @@ fn transpile_source_code_inner( if let Some(mi) = module_info.as_deref_mut() { mi.flags.has_tla = !parse_result.ast.top_level_await_keyword.is_empty(); } - // Derive the `*mut` from a `&mut` borrow (not `&x as *const _ - // as *mut _`, which is Stacked-Borrows UB). The borrow ends - // here; the raw pointer stays valid until `module_info` is - // moved/touched again (after `print_with_source_map`). - let module_info_ptr: Option< - *mut bun_bundler::analyze_transpiled_module::ModuleInfo, - > = module_info.as_deref_mut().map(core::ptr::from_mut); // ── js_printer::print ─────────────────────────────────────── // SAFETY: `extra.source_code_printer` is non-null per `TranspileExtra` @@ -2941,7 +2915,7 @@ fn transpile_source_code_inner( &mut *(*extra).source_code_printer, bun_js_printer::Format::EsmAscii, mapper.get(), - module_info_ptr, + module_info.as_deref_mut(), ) }; // The printer never took ownership of `module_info`; @@ -5243,7 +5217,7 @@ pub(crate) fn __bun_stdio_blob_store_new( mode, ..Default::default() }), - mime_type: bun_http_types::MimeType::NONE, + mime_type: bun_jsc::JsCell::new(bun_http_types::MimeType::NONE), ref_count: bun_ptr::ThreadSafeRefCount::init_exact_refs(2), is_all_ascii: None, }); diff --git a/src/runtime/napi/napi_body.rs b/src/runtime/napi/napi_body.rs index 4b8ef1c1cbe4..39380db18f7e 100644 --- a/src/runtime/napi/napi_body.rs +++ b/src/runtime/napi/napi_body.rs @@ -2429,10 +2429,6 @@ impl TsfnQueue { // Drop on TsfnQueue: LinearFifo drops itself. impl ThreadSafeFunction { - pub fn new(init: ThreadSafeFunction) -> *mut ThreadSafeFunction { - bun_core::heap::into_raw(Box::new(init)) - } - // This has two states: // 1. We need to run potentially multiple tasks. // 2. We need to finalize the ThreadSafeFunction. @@ -2784,7 +2780,7 @@ pub(super) extern "C" fn napi_create_threadsafe_function( }) }; - let function = ThreadSafeFunction::new(ThreadSafeFunction { + let mut function = Box::new(ThreadSafeFunction { // SAFETY: `event_loop()` is the live JS-thread loop (non-null, stable // address) and outlives every threadsafe function. event_loop: unsafe { bun_ptr::BackRef::from_raw(vm.event_loop()) }, @@ -2806,14 +2802,11 @@ pub(super) extern "C" fn napi_create_threadsafe_function( aborted: AtomicBool::new(true), }); - // SAFETY: function is non-null (just allocated). - let function_ref = unsafe { &mut *function }; - // nodejs by default keeps the event loop alive until the thread-safe function is unref'd - function_ref.ref_(); - function_ref.tracker.did_schedule(vm.global()); + function.ref_(); + function.tracker.did_schedule(vm.global()); - *result = function; + *result = bun_core::heap::into_raw(function); env.ok() } diff --git a/src/runtime/node/node_cluster_binding.rs b/src/runtime/node/node_cluster_binding.rs index 9da6e3777cb2..66f989907b61 100644 --- a/src/runtime/node/node_cluster_binding.rs +++ b/src/runtime/node/node_cluster_binding.rs @@ -33,20 +33,19 @@ unsafe extern "C" { pub(crate) static CHILD_SINGLETON: bun_core::RacyCell> = bun_core::RacyCell::new(None); -/// `&mut` to the (lazily-initialized) JS-thread singleton. +/// Shared reference to the (lazily-initialized) JS-thread singleton. /// -/// Centralises the `RacyCell> → &mut InternalMsgHolder` deref so the +/// Centralises the `RacyCell> → &InternalMsgHolder` deref so the /// three host-fn callers stay safe at the call site (PORTING.md §Global mutable -/// state — same shape as `cron::vm_mut`). Callers must be on the JS thread and -/// must not hold the borrow across a re-entrant `child_singleton()` call. +/// state — same shape as `cron::vm_mut`). Mutation goes through the holder's +/// `JsCell`/`Cell` fields, so a re-entrant `child_singleton()` is harmless. #[inline] -fn child_singleton<'a>() -> &'a mut InternalMsgHolder { +fn child_singleton<'a>() -> &'a InternalMsgHolder { // SAFETY: only called on the single JS thread. // `RacyCell::get` returns `*mut Option<_>`; the `Option` lives in - // `'static` storage so the returned `&mut` is valid for any caller-chosen - // `'a`. Aliasing: each of the three callers borrows for a single - // statement/block with no nested call to this fn. - unsafe { (*CHILD_SINGLETON.get()).get_or_insert_with(Default::default) } + // `'static` storage so the returned reference is valid for any + // caller-chosen `'a`. No `&mut` is ever handed out. + unsafe { &*(*CHILD_SINGLETON.get()).get_or_insert_with(Default::default) } } #[bun_jsc::host_fn] @@ -77,14 +76,20 @@ pub(crate) fn send_helper_child(global: &JSGlobalObject, frame: &CallFrame) -> J if callback.is_function() { // TODO: remove this strong. This is expensive and would be an easy way to create a memory leak. // These sequence numbers shouldn't exist from JavaScript's perspective at all. - let _ = singleton - .callbacks - .put(singleton.seq, StrongOptional::create(callback, global)); + let seq = singleton.seq.get(); + let strong = StrongOptional::create(callback, global); + singleton.callbacks.with_mut(|c| { + let _ = c.put(seq, strong); + }); } // sequence number for InternalMsgHolder - message.put(global, b"seq", JSValue::js_number(singleton.seq as f64)); - singleton.seq = singleton.seq.wrapping_add(1); + message.put( + global, + b"seq", + JSValue::js_number(singleton.seq.get() as f64), + ); + singleton.seq.set(singleton.seq.get().wrapping_add(1)); // similar code as Bun__Process__send #[cfg(debug_assertions)] @@ -147,8 +152,12 @@ pub(crate) fn on_internal_message_child( let arguments = frame.arguments_old::<2>().ptr; let singleton = child_singleton(); // TODO: we should not create two jsc.Strong.Optional here. If absolutely necessary, a single Array. should be all we use. - singleton.worker = StrongOptional::create(arguments[0], global); - singleton.cb = StrongOptional::create(arguments[1], global); + singleton + .worker + .set(StrongOptional::create(arguments[0], global)); + singleton + .cb + .set(StrongOptional::create(arguments[1], global)); singleton.flush(global)?; Ok(JSValue::UNDEFINED) } @@ -190,19 +199,23 @@ pub(crate) fn send_helper_primary(global: &JSGlobalObject, frame: &CallFrame) -> return Err(global.throw_invalid_argument_type_value("message", "object", message)); } if callback.is_function() { - let _ = ipc_data.internal_msg_queue.callbacks.put( - ipc_data.internal_msg_queue.seq, - StrongOptional::create(callback, global), - ); + let seq = ipc_data.internal_msg_queue.seq.get(); + let strong = StrongOptional::create(callback, global); + ipc_data.internal_msg_queue.callbacks.with_mut(|c| { + let _ = c.put(seq, strong); + }); } // sequence number for InternalMsgHolder message.put( global, b"seq", - JSValue::js_number(ipc_data.internal_msg_queue.seq as f64), + JSValue::js_number(ipc_data.internal_msg_queue.seq.get() as f64), ); - ipc_data.internal_msg_queue.seq = ipc_data.internal_msg_queue.seq.wrapping_add(1); + ipc_data + .internal_msg_queue + .seq + .set(ipc_data.internal_msg_queue.seq.get().wrapping_add(1)); // similar code as bun.jsc.Subprocess.doSend #[cfg(debug_assertions)] @@ -241,8 +254,14 @@ pub(crate) fn on_internal_message_primary( return Ok(JSValue::UNDEFINED); }; // TODO: remove these strongs. - ipc_data.internal_msg_queue.worker = StrongOptional::create(arguments[1], global); - ipc_data.internal_msg_queue.cb = StrongOptional::create(arguments[2], global); + ipc_data + .internal_msg_queue + .worker + .set(StrongOptional::create(arguments[1], global)); + ipc_data + .internal_msg_queue + .cb + .set(StrongOptional::create(arguments[2], global)); Ok(JSValue::UNDEFINED) } @@ -270,15 +289,19 @@ pub(crate) fn handle_internal_message_primary( let entry = ipc_data .internal_msg_queue .callbacks + .get() .get(&ack) .map(|s| s.get()); if let Some(callback_opt) = entry { - ipc_data.internal_msg_queue.callbacks.swap_remove(&ack); + ipc_data.internal_msg_queue.callbacks.with_mut(|c| { + c.swap_remove(&ack); + }); let cb = callback_opt.unwrap(); + let worker = ipc_data.internal_msg_queue.worker.get().get().unwrap(); event_loop.run_callback( cb, global, - ipc_data.internal_msg_queue.worker.get().unwrap(), + worker, &[ message, JSValue::NULL, // handle @@ -288,11 +311,12 @@ pub(crate) fn handle_internal_message_primary( } } } - let cb = ipc_data.internal_msg_queue.cb.get().unwrap(); + let cb = ipc_data.internal_msg_queue.cb.get().get().unwrap(); + let worker = ipc_data.internal_msg_queue.worker.get().get().unwrap(); event_loop.run_callback( cb, global, - ipc_data.internal_msg_queue.worker.get().unwrap(), + worker, &[ message, JSValue::NULL, // handle diff --git a/src/runtime/node/node_crypto_binding.rs b/src/runtime/node/node_crypto_binding.rs index a7553186d53b..37ee62130f9c 100644 --- a/src/runtime/node/node_crypto_binding.rs +++ b/src/runtime/node/node_crypto_binding.rs @@ -125,9 +125,9 @@ macro_rules! extern_crypto_job { } #[unsafe(export_name = concat!("Bun__", $name_str, "__schedule"))] - pub(crate) extern "C" fn __schedule(this: &mut Job) { - // SAFETY: `this` is a live pointer returned by `__create`. - unsafe { Job::schedule(this) }; + pub(crate) extern "C" fn __schedule(this: *mut Job) { + // SAFETY: `this` is the owned, unscheduled pointer from `__create`. + Job::schedule(unsafe { bun_core::heap::take(this) }); } #[unsafe(export_name = concat!("Bun__", $name_str, "__createAndSchedule"))] diff --git a/src/runtime/node/node_fs.rs b/src/runtime/node/node_fs.rs index e429b205a380..dd7d54b0ef31 100644 --- a/src/runtime/node/node_fs.rs +++ b/src/runtime/node/node_fs.rs @@ -955,9 +955,10 @@ mod _async_tasks { } pub fn run_from_js_thread(&mut self) -> Result<(), bun_jsc::JsTerminated> { - // SAFETY: self was Box::leak'd in create(); destroy() runs exactly once on scope exit - let _deinit = - scopeguard::guard(core::ptr::from_mut(self), |p| unsafe { Self::destroy(p) }); + let _deinit = scopeguard::guard(core::ptr::from_mut(self), |p| { + // SAFETY: self was Box::leak'd in create(); reclaimed exactly once on scope exit + Self::destroy(unsafe { bun_core::heap::take(p) }) + }); // Move `result` out so the `global_object()` `&self` borrow can coexist // with `&mut result` below; the sentinel left behind is dropped in `destroy()`. let mut result = core::mem::replace(&mut self.result, Err(sys::Error::default())); @@ -991,16 +992,10 @@ mod _async_tasks { Ok(()) } - /// SAFETY: `this` must be the pointer Box::leak'd in `create()`; called exactly once. - pub unsafe fn destroy(this: *mut Self) { - // SAFETY: caller guarantees `this` is a live Box-leaked allocation - let this_ref = unsafe { &mut *this }; - // `bun_sys::Error` frees its path on Drop. - this_ref.r#ref.unref(bun_io::js_vm_ctx()); - // `args: ThreadSafe` unprotects + drops via `heap::take` below. - this_ref.promise = JSPromiseStrong::default(); - // SAFETY: paired with Box::leak in create() - drop(unsafe { bun_core::heap::take(this) }); + pub fn destroy(mut self: Box) { + self.r#ref.unref(bun_io::js_vm_ctx()); + // Dropping `self` releases `promise`, unprotects+drops `args`, and lets + // `result`'s `bun_sys::Error` free its path — in field order. } } @@ -1311,8 +1306,9 @@ mod _async_tasks { pub fn run_from_js_thread(&mut self) -> Result<(), bun_jsc::JsTerminated> { // SAFETY: self was Box::leak'd in create(); destroy() runs exactly once on scope exit - let _deinit = scopeguard::guard(std::ptr::from_mut::(self), |p| unsafe { - Self::destroy(p) + let _deinit = scopeguard::guard(std::ptr::from_mut::(self), |p| { + // SAFETY: `p` is that Box::leak'd allocation, reclaimed exactly once here. + Self::destroy(unsafe { bun_core::heap::take(p) }) }); // Move `result` out so the `global_object()` `&self` borrow can coexist // with `&mut result` below; the sentinel left behind is dropped in `destroy()`. @@ -1356,17 +1352,18 @@ mod _async_tasks { Ok(()) } - /// SAFETY: `this` must be the pointer Box::leak'd in `create()`; called exactly once. - pub unsafe fn destroy(this: *mut Self) { - // SAFETY: caller guarantees `this` is a live Box-leaked allocation - let this_ref = unsafe { &mut *this }; + /// Takes the `Box`, so the ownership transfer is in the signature. Must keep + /// the same signature as `UVFSRequest::destroy`: both are reached through the + /// one `for_each_fs_async_op!` table in `dispatch.rs`, and `UVFSRequest` is a + /// distinct struct on Windows but an alias for this type everywhere else. + // `boxed_local`: the `Box` is the ownership unit being reclaimed here. + #[allow(clippy::boxed_local)] + pub fn destroy(mut this: Box) { // `bun_sys::Error` frees its path on Drop. // SAFETY: global_object outlives task; JSC_BORROW per LIFETIMES.tsv. - this_ref.r#ref.unref(bun_io::js_vm_ctx()); - // `args: ThreadSafe` unprotects + drops via `heap::take` below. - this_ref.promise = JSPromiseStrong::default(); - // SAFETY: paired with Box::leak in create() - drop(unsafe { bun_core::heap::take(this) }); + this.r#ref.unref(bun_io::js_vm_ctx()); + // `args: ThreadSafe` unprotects + drops with `this`. + this.promise = JSPromiseStrong::default(); } } @@ -1728,7 +1725,7 @@ mod _async_tasks { // outlives this task; `cp_on_finish` enqueues it concurrently. unsafe { ShellCpTask::cp_on_finish(shelltask, result) }; // SAFETY: self was Box::leak'd in create*(); destroyed exactly once here - unsafe { Self::destroy(std::ptr::from_mut::(self)) }; + Self::destroy(unsafe { bun_core::heap::take(std::ptr::from_mut::(self)) }); return Ok(()); } let go_ptr = self.evtloop.global_object(); @@ -1771,7 +1768,7 @@ mod _async_tasks { let _dispatch = self.tracker.dispatch(global_object); // SAFETY: self was Box::leak'd in create*(); destroyed exactly once here - unsafe { Self::destroy(std::ptr::from_mut::(self)) }; + Self::destroy(unsafe { bun_core::heap::take(std::ptr::from_mut::(self)) }); // SAFETY: `promise` points at a GC-rooted JS heap cell (see above), still // valid after `destroy` dropped only the `Strong` wrapper. let promise = unsafe { &mut *promise }; @@ -1783,24 +1780,16 @@ mod _async_tasks { Ok(()) } - /// SAFETY: `this` must be the pointer returned by Box::leak in - /// `create_with_shell_task()`/`create_mini()`; called exactly once. - pub unsafe fn destroy(this: *mut Self) { - // SAFETY: caller guarantees `this` is a live Box-leaked allocation - let this_ref = unsafe { &mut *this }; - // `bun_sys::Error` owns its path slice (`Box<[u8]>`) and frees it on - // Drop (in `heap::take` below). + // `boxed_local`: the `Box` is the ownership unit being reclaimed here. + #[allow(clippy::boxed_local)] + pub fn destroy(mut this: Box) { if !IS_SHELL { - this_ref - .r#ref - .unref(event_loop_handle_to_ctx(this_ref.evtloop)); + this.r#ref.unref(event_loop_handle_to_ctx(this.evtloop)); } - // `args.deinit()` → `Drop` on `args::Cp` (via `heap::take` below). + // Released here, before the remaining fields drop below. + this.promise = JSPromiseStrong::default(); // `Drop for ThreadSafe` releases the `protect()` taken by // `to_thread_safe()` when `src`/`dest` are Buffers, so nothing leaks here. - this_ref.promise = JSPromiseStrong::default(); - // SAFETY: paired with Box::leak in create_with_shell_task()/create_mini() - drop(unsafe { bun_core::heap::take(this) }); } /// Directory scanning + clonefile will block this thread, then each individual file copy (what the sync version @@ -2621,8 +2610,8 @@ mod _async_tasks { let _dispatch = self.tracker.dispatch(global_object); - // SAFETY: self was Box::leak'd in create(); destroyed exactly once here - unsafe { Self::destroy(std::ptr::from_mut::(self)) }; + // SAFETY: self was Box::leak'd in create(); reclaimed exactly once here. + Self::destroy(unsafe { bun_core::heap::take(std::ptr::from_mut::(self)) }); // SAFETY: GC-rooted JS heap cell, valid past `destroy` (see above). let promise = unsafe { &mut *promise }; if success { @@ -2633,22 +2622,22 @@ mod _async_tasks { Ok(()) } - /// SAFETY: `this` must be the pointer Box::leak'd in `create()`; called exactly once. - pub unsafe fn destroy(this: *mut Self) { - // SAFETY: caller guarantees `this` is a live Box-leaked allocation - let this_ref = unsafe { &mut *this }; - debug_assert!(this_ref.root_fd == FD::INVALID); // should already have closed it + /// Takes the `Box`. Same signature as `AsyncFSTask::destroy` / + /// `UVFSRequest::destroy`: all three are reached through the one + /// `for_each_fs_async_op!` table in `dispatch.rs`. + // `boxed_local`: the `Box` is the ownership unit being reclaimed here. + #[allow(clippy::boxed_local)] + pub fn destroy(mut this: Box) { + debug_assert!(this.root_fd == FD::INVALID); // should already have closed it // `bun_sys::Error` frees on Drop; nothing to do. - let _ = this_ref.pending_err.take(); + let _ = this.pending_err.take(); // `KeepAlive::unref` takes the type-erased // `EventLoopCtx`. Resolve via the global JS-loop hook (single JS thread). - this_ref.r#ref.unref(bun_io::js_vm_ctx()); - // `args.deinit()` → `Drop` on `args::Readdir` (via `heap::take` below). - this_ref.free_root_path(); - this_ref.clear_result_list(); - // `JSPromiseStrong` releases on Drop (via heap::take below). - // SAFETY: paired with Box::leak in create() - drop(unsafe { bun_core::heap::take(this) }); + this.r#ref.unref(bun_io::js_vm_ctx()); + // `args.deinit()` → `Drop` on `args::Readdir` when `this` drops. + this.free_root_path(); + this.clear_result_list(); + // `JSPromiseStrong` releases on Drop. } } @@ -5888,10 +5877,6 @@ impl NodeFS { // `OSPathBuffer = [u16; PATH_MAX_WIDE]` (65 534 B) which fits inside // `PathBuffer` (`MAX_PATH_BYTES` = 98 302 B); on POSIX it is the same // type. The `assert!` below verifies the alignment at runtime. - // Keep the raw `*mut PathBuffer` so error-return paths can re-derive a fresh - // `&mut PathBuffer` without reborrowing `&mut self` (which would alias - // `working_mem` under stacked borrows). On every such path `working_mem` is - // not used afterward, so the re-derive is sound. let sync_error_buf_ptr: *mut PathBuffer = &raw mut self.sync_error_buf; assert!( sync_error_buf_ptr.cast::().is_aligned(), @@ -5929,17 +5914,15 @@ impl NodeFS { { // is a directory. break. if !res { - // SAFETY: `working_mem` is not used after this return; re-derive - // the &mut PathBuffer from the stored raw ptr instead of `&mut self`. - let buf = unsafe { &mut *sync_error_buf_ptr }; + // `working_mem` is not read after this return. return Err(sys::Error { errno: E::ENOTDIR as _, syscall: sys::Tag::mkdir, - path: Self::os_path_into_buf( - buf, - without_nt_prefix(&(&path[..])[..len as usize]), - ) - .into(), + path: self + .os_path_into_sync_error_buf(without_nt_prefix( + &(&path[..])[..len as usize], + )) + .into(), ..Default::default() }); } @@ -5958,17 +5941,14 @@ impl NodeFS { #[cfg(windows)] let p = { // `parent` aliases `working_mem` (== sync_error_buf). Copy it - // out to a temp before re-deriving `&mut PathBuffer` so we + // out to a temp before writing into `sync_error_buf` so we // never hold `&mut buf` and `&buf[..]` simultaneously. + // `working_mem`/`parent` are not read after this return. let stripped = without_nt_prefix(&parent[..]); let n = stripped.len(); let mut tmp = paths::os_path_buffer_pool::get(); tmp[..n].copy_from_slice(stripped); - // SAFETY: `working_mem`/`parent` are not used after this return. - Self::os_path_into_buf( - unsafe { &mut *sync_error_buf_ptr }, - &tmp[..n], - ) + self.os_path_into_sync_error_buf(&tmp[..n]) }; #[cfg(not(windows))] let p = without_nt_prefix(&parent[..]); @@ -6003,12 +5983,10 @@ impl NodeFS { E::EEXIST => {} // NOENT shouldn't happen here _ => { - // SAFETY: `working_mem` is not used after this return. - let buf = unsafe { &mut *sync_error_buf_ptr }; - return Err(err.with_path(Self::os_path_into_buf( - buf, - without_nt_prefix(&path[..]), - ))); + // `working_mem` is not read after this return. + return Err(err.with_path( + self.os_path_into_sync_error_buf(without_nt_prefix(&path[..])), + )); } } } @@ -6032,11 +6010,10 @@ impl NodeFS { Err(err) => match err.get_errno() { E::EEXIST => {} _ => { - // SAFETY: `working_mem` is not used after this return. - let buf = unsafe { &mut *sync_error_buf_ptr }; - return Err( - err.with_path(Self::os_path_into_buf(buf, without_nt_prefix(&path[..]))) - ); + // `working_mem`/`final_` are not read after this return. + return Err(err.with_path( + self.os_path_into_sync_error_buf(without_nt_prefix(&path[..])), + )); } }, Ok(_) => {} @@ -8233,9 +8210,7 @@ impl NodeFS { } /// Free-function form of [`os_path_into_sync_error_buf`] that does not borrow - /// `&mut self`. Needed by `mkdir_recursive_os_path_impl`, which holds a long-lived - /// `&mut OSPathBuffer` reinterpreted from `sync_error_buf` and so must not reborrow - /// `&mut self` on its error-return paths (PORTING.md §Forbidden aliased `&mut`). + /// `&mut self`. fn os_path_into_buf<'a>(buf: &'a mut PathBuffer, slice: &[OSPathChar]) -> &'a [u8] { #[cfg(windows)] { diff --git a/src/runtime/node/node_fs_watcher.rs b/src/runtime/node/node_fs_watcher.rs index 32fe842073d6..2807666d8738 100644 --- a/src/runtime/node/node_fs_watcher.rs +++ b/src/runtime/node/node_fs_watcher.rs @@ -76,11 +76,11 @@ pub mod js { impl FSWatcher { #[inline] - fn vm(&self) -> &'static mut VirtualMachine { + fn vm(&self) -> &'static VirtualMachine { // SAFETY: BACKREF — `ctx` is the per-thread `VirtualMachine` singleton // (set in `init` from `globalThis.bunVM()`); it outlives every // FSWatcher and all access is on the JS thread. - unsafe { &mut *self.ctx } + unsafe { &*self.ctx } } #[inline] @@ -98,8 +98,8 @@ impl FSWatcher { /// the caller releases ownership of; the concurrent queue takes ownership /// and frees it on the JS thread after dispatch. pub fn enqueue_task_concurrent(&self, task: core::ptr::NonNull) { - // `vm()` is the BACKREF accessor; `event_loop_shared()` is the audited - // safe `&EventLoop` accessor. `enqueue_task_concurrent` is the + // `vm()` is the shared BACKREF accessor; `event_loop_shared()` is the + // audited safe `&EventLoop` accessor. `enqueue_task_concurrent` is the // documented cross-thread entry point and only touches the lock-free // queue. self.vm().event_loop_shared().enqueue_task_concurrent(task); @@ -264,21 +264,15 @@ impl FSWatchTaskPosix { /// the assert below enforces that. A `Drop` impl would also fire on /// `*self = Self{..}` in `append()` and on `heap::take` in `finalize`, /// where `self` *is* `current_task`, which would always trip the assert. - /// - /// # Safety - /// `this` must be the unique `heap::alloc` pointer produced by - /// `enqueue()`; called from the JS-thread task dispatcher only. - pub unsafe fn deinit(this: *mut Self) { - // SAFETY: caller contract — `this` is the live heap clone. - let this_ref = unsafe { &mut *this }; - this_ref.clean_entries(); + // `boxed_local`: the `Box` is the ownership unit being reclaimed here. + #[allow(clippy::boxed_local)] + pub fn deinit(mut self: Box) { + self.clean_entries(); #[cfg(debug_assertions)] - { - // SAFETY: ctx is valid for the lifetime of any task (ParentRef). - debug_assert!(!core::ptr::eq(this_ref.ctx().current_task.as_ptr(), this)); - } - // SAFETY: paired with `heap::alloc` in `enqueue()`. - drop(unsafe { bun_core::heap::take(this) }); + debug_assert!(!core::ptr::eq( + self.ctx().current_task.as_ptr(), + &raw const *self + )); } } @@ -430,8 +424,8 @@ impl FSWatchTaskWindows { event: Event::Abort, })); - // `ctx` is the live owning `ParentRef` (BACKREF); `vm()` → - // `event_loop_mut()` is the audited safe `&mut EventLoop` accessor. + // `ctx` is the live owning `ParentRef` (BACKREF); `vm()` is + // shared, `event_loop_mut()` is the audited `&mut EventLoop` accessor. // Ownership of `task` transfers to the queue (drained on the same thread). ctx.expect("FSWatchTask.ctx unset") .vm() @@ -494,16 +488,12 @@ impl FSWatchTaskWindows { /// `FSWatchTaskWindows.deinit`. Explicit, not /// `impl Drop`, to mirror `FSWatchTaskPosix::deinit` so the dispatcher can /// call `FSWatchTask::deinit` uniformly. - /// - /// # Safety - /// `this` must be the unique `heap::alloc` pointer produced by - /// `append_abort()` / `on_path_update_windows()`. - pub unsafe fn deinit(this: *mut Self) { + // `boxed_local`: the `Box` is the ownership unit being reclaimed here. + #[allow(clippy::boxed_local)] + pub fn deinit(self: Box) { // `Event` (and `StringOrBytesToDecode`, via its explicit `Drop` impl // above which `deref()`s the WTF string) free their payloads via Drop, // so dropping the Box releases everything. - // SAFETY: paired with `heap::alloc` at the enqueue site. - drop(unsafe { bun_core::heap::take(this) }); } } @@ -581,9 +571,9 @@ impl FSWatcher { ctx: Some(unsafe { bun_ptr::ParentRef::from_raw_mut(this.as_ctx_ptr()) }), event, })); - // `vm()` is the BACKREF accessor; `event_loop_mut()` is the audited - // safe `&mut EventLoop` accessor. Ownership of `task` transfers to the - // queue. + // `vm()` is the shared BACKREF accessor; `event_loop_mut()` is the + // audited `&mut EventLoop` accessor. Ownership of `task` transfers to + // the queue. this.vm().event_loop_mut().enqueue_task(Task::init(task)); let _ = is_file; } diff --git a/src/runtime/node/types.rs b/src/runtime/node/types.rs index 1ec7ccf4c0e2..84fb8bd2bbc5 100644 --- a/src/runtime/node/types.rs +++ b/src/runtime/node/types.rs @@ -1472,25 +1472,36 @@ unsafe extern "C" { ) -> i32; } -unsafe extern "C" fn append_buffer_span( +/// Sink for `Bun__JSArray__collectBufferSpans`. The trampoline below does the +/// single deref of the opaque ctx; impls only ever see `&mut self`. +trait BufferSpanSink { + fn append_span(&mut self, element: JSValue, span: &mut [u8]); +} + +impl BufferSpanSink for VectorArrayBuffer { + fn append_span(&mut self, element: JSValue, span: &mut [u8]) { + self.buffers.push(bun_sys::platform_iovec_create(span)); + self.views.push(element); + } +} + +unsafe extern "C" fn append_buffer_span( ctx: *mut std::ffi::c_void, element: JSValue, data: *mut u8, byte_len: usize, ) { - // SAFETY: `ctx` is the `&mut VectorArrayBuffer` passed to - // `Bun__JSArray__collectBufferSpans` by `from_js` below, alive for the - // duration of the call. - let out = unsafe { &mut *ctx.cast::() }; - let slice: &mut [u8] = if data.is_null() || byte_len == 0 { + let span: &mut [u8] = if data.is_null() || byte_len == 0 { &mut [] } else { // SAFETY: `data..data + byte_len` is the byte range of `element`'s // backing store, valid and unaliased for the duration of the callback. unsafe { std::slice::from_raw_parts_mut(data, byte_len) } }; - out.buffers.push(bun_sys::platform_iovec_create(slice)); - out.views.push(element); + // SAFETY: `ctx` is the `*mut T` passed to `Bun__JSArray__collectBufferSpans`, + // alive for the duration of the call. `append_span` cannot reach JS, so the + // `&mut T` it borrows here cannot alias. + unsafe { (*ctx.cast::()).append_span(element, span) }; } impl VectorArrayBuffer { @@ -1522,7 +1533,7 @@ impl VectorArrayBuffer { val, pin, (&raw mut out).cast(), - append_buffer_span, + append_buffer_span::, ) }; scope.assert_exception_presence_matches(status == -1); diff --git a/src/runtime/node/win_watcher.rs b/src/runtime/node/win_watcher.rs index 8aeb2ad997fb..7c85b6888f98 100644 --- a/src/runtime/node/win_watcher.rs +++ b/src/runtime/node/win_watcher.rs @@ -2,12 +2,13 @@ #![cfg(windows)] +use core::cell::Cell; use core::ffi::{c_char, c_int, c_void}; use core::ptr; use bun_collections::{ArrayHashMap, StringArrayHashMap}; use bun_core::{String as BunString, ZStr}; -use bun_jsc as jsc; +use bun_jsc::{self as jsc, JsCell}; use bun_paths::PathBuffer; use bun_sys as sys; use bun_sys::ReturnCodeExt as _; @@ -58,96 +59,105 @@ static DEFAULT_MANAGER_MUTEX: Mutex = Mutex::new(); pub(crate) struct PathWatcherManager { // Keys are owned path bytes, values are raw heap // PathWatcher ptrs. `StringArrayHashMap` lets `get`/`insert` take `&[u8]` borrows. - watchers: StringArrayHashMap<*mut PathWatcher>, + watchers: JsCell>, // LIFETIMES.tsv: JSC_BORROW → `&VirtualMachine`. The manager is heap-allocated and stored in a // process-global, so we spell the borrow as `'static`; soundness relies on // the owning VM outliving the manager (watchers are torn down before the VM). vm: &'static jsc::VirtualMachineRef, - deinit_on_last_watcher: bool, + deinit_on_last_watcher: Cell, } impl PathWatcherManager { pub(crate) fn init(vm: &'static jsc::VirtualMachineRef) -> *mut PathWatcherManager { bun_core::heap::into_raw(Box::new(PathWatcherManager { - watchers: StringArrayHashMap::default(), + watchers: JsCell::new(StringArrayHashMap::default()), vm, // A manager can be displaced from `DEFAULT_MANAGER` by a `watch()` // call from a different VM; without this the displaced manager // would never be freed. Set here — on the owning thread, before the // pointer is published — to avoid a cross-thread write at // displacement time. - deinit_on_last_watcher: true, + deinit_on_last_watcher: Cell::new(true), })) } /// unregister is always called from main thread - fn unregister_watcher(&mut self, watcher: *mut PathWatcher, path: &ZStr) { + /// + /// # Safety + /// `this` must be a live manager pointer from [`Self::init`]; it may be freed here. + unsafe fn unregister_watcher( + this: *mut PathWatcherManager, + watcher: *mut PathWatcher, + path: &ZStr, + ) { #[cfg(not(debug_assertions))] let _ = path; - if let Some(index) = self.watchers.values().iter().position(|&w| w == watcher) { - #[cfg(debug_assertions)] - { - if !path.as_bytes().is_empty() { - debug_assert!(&*self.watchers.keys()[index] == path.as_bytes()); + // SAFETY: caller guarantees `this` is a live heap-allocated pointer (see `init`). + let me = unsafe { &*this }; + me.watchers.with_mut(|watchers| { + if let Some(index) = watchers.values().iter().position(|&w| w == watcher) { + #[cfg(debug_assertions)] + { + if !path.as_bytes().is_empty() { + debug_assert!(&*watchers.keys()[index] == path.as_bytes()); + } } - } - // Key is `Box<[u8]>`; swap_remove drops it (replaces `allocator.free(keys[index])`). - self.watchers.swap_remove_at(index); - } + // Key is `Box<[u8]>`; swap_remove drops it (replaces `allocator.free(keys[index])`). + watchers.swap_remove_at(index); + } + }); - // No early returns above, so this runs unconditionally — and avoids the - // overlapping `&mut self` borrow a closure-based guard would require. - if self.deinit_on_last_watcher && self.watchers.len() == 0 { - // SAFETY: self was heap-allocated in `init`; no other live borrows after this point. - unsafe { Self::deinit(core::ptr::from_mut(self)) }; + // No early returns above, so this runs unconditionally. + if me.deinit_on_last_watcher.get() && me.watchers.get().len() == 0 { + // SAFETY: `this` was produced by heap::into_raw in `init`. + unsafe { bun_core::heap::take(this) }.deinit(); } } - /// Tear down the manager. Takes a raw pointer because it frees `self`. + /// Tear down the manager. Consumes the box because it frees `self`. /// /// NOTE: not `impl Drop` — this type is always held via `*mut` (global static + BACKREF from - /// PathWatcher) and self-frees via `heap::take`. - unsafe fn deinit(this: *mut PathWatcherManager) { + /// PathWatcher), so destruction is spelled as an explicit `Box` consumer. + fn deinit(self: Box) { // enable to create a new manager { let _g = DEFAULT_MANAGER_MUTEX.lock_guard(); - if DEFAULT_MANAGER.load() == this { + if DEFAULT_MANAGER.load() == core::ptr::from_ref(&*self).cast_mut() { DEFAULT_MANAGER.store(ptr::null_mut()); } } - // SAFETY: caller guarantees `this` is a live heap-allocated pointer (see `init`). - let me = unsafe { &mut *this }; - - if me.watchers.len() != 0 { - me.deinit_on_last_watcher = true; + if self.watchers.get().len() != 0 { + self.deinit_on_last_watcher.set(true); + // Still reachable through every watcher's BACKREF; the last unregister frees it. + let _ = bun_core::heap::into_raw(self); return; } - for &watcher in me.watchers.values() { + for &watcher in self.watchers.get().values() { // SAFETY: watcher pointers are valid until their own deinit runs. unsafe { - (*watcher).manager = None; - PathWatcher::deinit(watcher); + (*watcher).manager.set(None); + bun_core::heap::take(watcher).deinit(); } } // Keys (`Box<[u8]>`) are dropped by the map's Drop — replaces the explicit // `allocator.free(path)` loop + `watchers.deinit(allocator)`. - // SAFETY: `this` was produced by heap::alloc in `init`. - drop(unsafe { bun_core::heap::take(this) }); } } // ────────────────────────────────────────────────────────────────────────── pub struct PathWatcher { - handle: uv::uv_fs_event_t, + // `JsCell` is `repr(transparent)` over `UnsafeCell`, so `offset_of!` (and therefore + // `from_field_ptr!`) is unchanged while libuv may mutate the handle behind `&PathWatcher`. + handle: JsCell, // LIFETIMES.tsv: BACKREF → Option<*mut PathWatcherManager> - manager: Option<*mut PathWatcherManager>, - emit_in_progress: bool, - handlers: ArrayHashMap<*mut c_void, ChangeEvent>, + manager: Cell>, + emit_in_progress: Cell, + handlers: JsCell>, } #[derive(Clone, Copy)] @@ -199,9 +209,7 @@ impl PathWatcher { events: c_int, status: uv::ReturnCode, ) { - // SAFETY: libuv guarantees `event` is the handle we registered; read `.data` - // through the raw pointer so we don't form a `&mut uv_fs_event_t` that would - // alias the `&mut PathWatcher` we derive below (Stacked Borrows). + // SAFETY: libuv guarantees `event` is the handle we registered. if unsafe { (*event).data }.is_null() { bun_core::debug_warn!("uvEventCallback called with null data"); return; @@ -210,27 +218,31 @@ impl PathWatcher { let this: *mut PathWatcher = unsafe { bun_core::from_field_ptr!(PathWatcher, handle, event) }; // SAFETY: `this` was heap-allocated in `init` and is kept alive until uv_close fires. - // This is the *only* live `&mut` covering the embedded handle for the rest of this fn. - let this = unsafe { &mut *this }; + // Shared, never `&mut`: every field touched below is a cell, so nothing aliases across + // the JS re-entry inside `on_path_update_fn`. + let me = unsafe { &*this }; #[cfg(debug_assertions)] { - debug_assert!(this.handle.data == this as *mut PathWatcher as *mut c_void); + debug_assert!(me.handle.get().data == this.cast::()); } // SAFETY: libuv contract — `loop_` is valid while the handle is open. - let timestamp = unsafe { (*this.handle.loop_).time }; + let timestamp = unsafe { (*me.handle.get().loop_).time }; if let Some(err) = status.to_error(sys::Tag::watch) { - this.emit_in_progress = true; + me.emit_in_progress.set(true); - for &ctx in this.handlers.keys() { + // Re-read the key each turn: the JS callback may mutate `handlers`. + for i in 0..me.handlers.get().len() { + let ctx = me.handlers.get().keys()[i]; on_path_update_fn(Some(ctx), Event::Error(err.clone()), false); on_update_end_fn(Some(ctx)); } // The guard is still `true` when `maybe_deinit` checks it (always a no-op there). - this.maybe_deinit(); - this.emit_in_progress = false; + // SAFETY: `this` is live; the guard makes this call a no-op. + unsafe { Self::maybe_deinit(this) }; + me.emit_in_progress.set(false); return; } @@ -244,40 +256,55 @@ impl PathWatcher { // ReadDirectoryChangesW overflowed and changes were lost (always // UV_CHANGE), or libuv could not convert the name to UTF-8. // Forward `(event, null)` to every handler like node, unsuppressed. - this.emit_in_progress = true; - for &ctx in this.handlers.keys() { + me.emit_in_progress.set(true); + for i in 0..me.handlers.get().len() { + let ctx = me.handlers.get().keys()[i]; on_path_update_fn(Some(ctx), Event::NoFilename(event_type), false); on_update_end_fn(Some(ctx)); } - this.emit_in_progress = false; - this.maybe_deinit(); + me.emit_in_progress.set(false); + // SAFETY: `this` is live; this may free it, and nothing reads it afterwards. + unsafe { Self::maybe_deinit(this) }; return; } // SAFETY: libuv passes a valid NUL-terminated string when non-null. let path = ZStr::from_cstr(unsafe { core::ffi::CStr::from_ptr(filename) }); // Intentional wrap to bun_watcher::HashType - let hash = this.handle.hash(path.as_bytes(), events, status) as bun_watcher::HashType; - let is_file = !this.handle.is_dir(); - this.emit(path.as_bytes(), hash, timestamp, is_file, event_type); + let hash = me.handle.get().hash(path.as_bytes(), events, status) as bun_watcher::HashType; + let is_file = !me.handle.get().is_dir(); + // SAFETY: `this` is live; `emit` may free it once the last handler is gone. + unsafe { Self::emit(this, path.as_bytes(), hash, timestamp, is_file, event_type) }; } - pub(crate) fn emit( - &mut self, + /// # Safety + /// `this` must be the live `PathWatcher` pointer from `init`; it may be freed here. + pub(crate) unsafe fn emit( + this: *mut PathWatcher, path: &[u8], hash: bun_watcher::HashType, timestamp: u64, is_file: bool, event_type: WatchEventKind, ) { - self.emit_in_progress = true; + // SAFETY: caller guarantees `this` is live. + let me = unsafe { &*this }; + me.emit_in_progress.set(true); #[cfg(debug_assertions)] let mut debug_count: usize = 0; - for i in 0..self.handlers.len() { - let event = &mut self.handlers.values_mut()[i]; - if event.emit(hash, timestamp, event_type) { - let ctx: *mut FSWatcher = self.handlers.keys()[i].cast::(); + for i in 0..me.handlers.get().len() { + // Take the dedupe decision and the ctx out, then drop the borrow: the calls below + // re-enter JS and may mutate `handlers`. + let (ctx, fire) = me.handlers.with_mut(|handlers| { + let ctx = handlers.keys()[i]; + ( + ctx, + handlers.values_mut()[i].emit(hash, timestamp, event_type), + ) + }); + if fire { + let ctx: *mut FSWatcher = ctx.cast::(); // SAFETY: handlers keys are `*mut FSWatcher` erased to `*mut c_void` in `watch()`. let encoding = unsafe { (*ctx).encoding }; // `EventPathString` on Windows is `StringOrBytesToDecode`. @@ -307,12 +334,15 @@ impl PathWatcher { debug_count, ); - self.emit_in_progress = false; - self.maybe_deinit(); + me.emit_in_progress.set(false); + // SAFETY: `this` is live; frees it once the last handler is gone. + unsafe { Self::maybe_deinit(this) }; } - pub(crate) fn init( - manager: &mut PathWatcherManager, + /// # Safety + /// `manager` must be a live pointer from [`PathWatcherManager::init`]. + pub(crate) unsafe fn init( + manager: *mut PathWatcherManager, path: &ZStr, recursive: bool, ) -> sys::Result<*mut PathWatcher> { @@ -335,18 +365,19 @@ impl PathWatcher { sys::Result::Ok(len) => ZStr::from_buf(outbuf.as_slice(), len), }; - // BACKREF field stays raw (LIFETIMES.tsv); capture the pointer once before further &mut use. - let manager_ptr: *mut PathWatcherManager = manager as *mut PathWatcherManager; + // SAFETY: caller guarantees `manager` is a live heap pointer. Only cells are mutated + // through it, so a shared borrow suffices; the BACKREF keeps the raw (it frees later). + let me = unsafe { &*manager }; - if let Some(&existing) = manager.watchers.get(event_path.as_bytes()) { + if let Some(&existing) = me.watchers.get().get(event_path.as_bytes()) { return sys::Result::Ok(existing); } let this_box = Box::new(PathWatcher { - handle: bun_core::ffi::zeroed(), - manager: Some(manager_ptr), - emit_in_progress: false, - handlers: ArrayHashMap::default(), + handle: JsCell::new(bun_core::ffi::zeroed()), + manager: Cell::new(Some(manager)), + emit_in_progress: Cell::new(false), + handlers: JsCell::new(ArrayHashMap::default()), }); let this = bun_core::heap::into_raw(this_box); @@ -354,20 +385,23 @@ impl PathWatcher { // bun.assert evaluates its argument before the inline early-return, so this runs in release too. // SAFETY: `this` is a freshly-allocated valid pointer; uv_loop comes from the VM. unsafe { - // `ptr::addr_of_mut!` (not `&mut (*this).handle`): libuv stashes this pointer and + // `ptr::addr_of_mut!` (not `&(*this).handle`): libuv stashes this pointer and // hands it back to `uv_event_callback`, which `from_field_ptr!`-offsets it to recover // the parent `PathWatcher`. Deriving via `addr_of_mut!` keeps `this`'s whole-allocation - // provenance so that container-of access stays in-bounds under Stacked Borrows. - let rc = uv::uv_fs_event_init(manager.vm.uv_loop(), ptr::addr_of_mut!((*this).handle)); + // provenance so that container-of access stays in-bounds. + let rc = + uv::uv_fs_event_init(me.vm.uv_loop(), ptr::addr_of_mut!((*this).handle).cast()); debug_assert!(rc == uv::ReturnCode::zero()); - (*this).handle.data = this.cast::(); + (*this) + .handle + .with_mut(|handle| handle.data = this.cast::()); } // UV_FS_EVENT_RECURSIVE only works for Windows and OSX // SAFETY: `(*this).handle` was initialized by uv_fs_event_init above; event_path is NUL-terminated. let start_rc = unsafe { uv::uv_fs_event_start( - ptr::addr_of_mut!((*this).handle), + ptr::addr_of_mut!((*this).handle).cast(), Some(PathWatcher::uv_event_callback), event_path.as_ptr().cast::(), if recursive { @@ -383,8 +417,8 @@ impl PathWatcher { // to swap_remove here. // SAFETY: `this` is the freshly heap-allocated pointer above; deinit consumes it. unsafe { - (*this).manager = None; // prevent deinit() from re-entering unregister_watcher - PathWatcher::deinit(this); + (*this).manager.set(None); // prevent deinit() from re-entering unregister_watcher + bun_core::heap::take(this).deinit(); } return sys::Result::Err(err); } @@ -394,7 +428,9 @@ impl PathWatcher { // Owned key: dupe of event_path bytes (the sentinel NUL is not part of the // slice's `.len`, so the StringArrayHashMap key compares equal to `event_path.as_bytes()`). - manager.watchers.insert(event_path.as_bytes(), this); + me.watchers.with_mut(|watchers| { + watchers.insert(event_path.as_bytes(), this); + }); sys::Result::Ok(this) } @@ -418,48 +454,58 @@ impl PathWatcher { pub(crate) fn detach(this: *mut PathWatcher, handler: *mut c_void) { // SAFETY: `this` is the live `heap::alloc`'d pointer returned from `watch()`; // it stays valid until `maybe_deinit` self-destroys on the last handler. - let me = unsafe { &mut *this }; - if me.handlers.swap_remove(&handler) { - me.maybe_deinit(); + let me = unsafe { &*this }; + if me + .handlers + .with_mut(|handlers| handlers.swap_remove(&handler)) + { + // SAFETY: same live pointer; `maybe_deinit` may free it. + unsafe { Self::maybe_deinit(this) }; } } - fn maybe_deinit(&mut self) { - if self.handlers.len() == 0 && !self.emit_in_progress { - // SAFETY: self was heap-allocated in `init`; no other live borrows after this point. - unsafe { Self::deinit(core::ptr::from_mut(self)) }; + /// # Safety + /// `this` must be the live `PathWatcher` pointer from `init`; it may be freed here. + unsafe fn maybe_deinit(this: *mut Self) { + // SAFETY: caller guarantees `this` is live. + let me = unsafe { &*this }; + if me.handlers.get().len() == 0 && !me.emit_in_progress.get() { + // SAFETY: `this` was heap-allocated in `init`; nothing reads it after this. + unsafe { bun_core::heap::take(this) }.deinit(); } } /// NOTE: not `impl Drop` — destruction is deferred through `uv_close` and the close callback - /// frees the box, so this type is always managed via raw `*mut PathWatcher`. - unsafe fn deinit(this: *mut PathWatcher) { + /// frees the box, so the open-handle path hands the allocation straight back to `into_raw`. + fn deinit(self: Box) { bun_output::scoped_log!(fs_watch, "deinit"); - // SAFETY: caller guarantees `this` is a live heap-allocated pointer (see `init`). - let me = unsafe { &mut *this }; - me.handlers.clear(); + self.handlers.with_mut(|handlers| handlers.clear()); - if let Some(manager) = me.manager.take() { - let path: &ZStr = if !me.handle.path.is_null() { + if let Some(manager) = self.manager.take() { + let handle = self.handle.get(); + let path: &ZStr = if !handle.path.is_null() { // SAFETY: handle.path is a NUL-terminated C string owned by libuv. - ZStr::from_cstr(unsafe { core::ffi::CStr::from_ptr(me.handle.path) }) + ZStr::from_cstr(unsafe { core::ffi::CStr::from_ptr(handle.path) }) } else { ZStr::EMPTY }; + // Only compared against the manager's map entries, never dereferenced there. + let this: *mut PathWatcher = core::ptr::from_ref(&*self).cast_mut(); // SAFETY: manager backref is valid until the manager deinits (see PathWatcherManager::deinit). - unsafe { (*manager).unregister_watcher(this, path) }; + unsafe { PathWatcherManager::unregister_watcher(manager, this, path) }; } // `UvHandle::is_closed` reads `flags & UV_HANDLE_CLOSED` via the handle prefix. - if me.handle.is_closed() { - // SAFETY: `this` was heap-allocated in `init`. - drop(unsafe { bun_core::heap::take(this) }); + if self.handle.get().is_closed() { + drop(self); } else { + // `uv_closed_callback` frees the box; release it before libuv takes the pointer. + let this = bun_core::heap::into_raw(self); // SAFETY: handle is open and not yet closing; stop/close are valid in that state. unsafe { - uv::uv_fs_event_stop(&mut me.handle); + uv::uv_fs_event_stop(ptr::addr_of_mut!((*this).handle).cast()); uv::uv_close( - ptr::addr_of_mut!(me.handle).cast(), + ptr::addr_of_mut!((*this).handle).cast(), Some(PathWatcher::uv_closed_callback), ); } @@ -505,15 +551,16 @@ pub fn watch( // SAFETY: `manager` is a live heap-allocated pointer bound to the calling // VM (created above or matched by `vm`). All other mutation of this manager // happens on this VM's thread, and concurrent `watch()` calls from other - // Workers are serialized by DEFAULT_MANAGER_MUTEX (still held here), so - // this `&mut` is unaliased for the call. - let watcher = match PathWatcher::init(unsafe { &mut *manager }, path, recursive) { + // Workers are serialized by DEFAULT_MANAGER_MUTEX (still held here). + let watcher = match unsafe { PathWatcher::init(manager, path, recursive) } { sys::Result::Err(err) => return sys::Result::Err(err), sys::Result::Ok(w) => w, }; // SAFETY: watcher is a valid freshly-returned heap pointer. unsafe { - (*watcher).handlers.insert(ctx, ChangeEvent::default()); + (*watcher).handlers.with_mut(|handlers| { + handlers.insert(ctx, ChangeEvent::default()); + }); } sys::Result::Ok(watcher) } diff --git a/src/runtime/node/zlib/NativeZlib.rs b/src/runtime/node/zlib/NativeZlib.rs index 66c9555bc5a4..c12234cd14e5 100644 --- a/src/runtime/node/zlib/NativeZlib.rs +++ b/src/runtime/node/zlib/NativeZlib.rs @@ -33,10 +33,9 @@ mod _impl { /// `bun.ptr.RefCount(@This(), "ref_count", deinit, .{})` — intrusive single-thread refcount. /// `ref`/`deref` are provided by `bun_ptr::IntrusiveRc`; when the count hits - /// zero it invokes [`NativeZlib::deinit`]. + /// zero the derive's default destructor drops the `Box`. #[bun_jsc::JsClass] #[derive(bun_ptr::CellRefCounted)] - #[ref_count(destroy = Self::deinit)] pub struct NativeZlib { pub ref_count: Cell, // JSC_BORROW backref; global outlives this m_ctx payload. `BackRef` @@ -249,19 +248,14 @@ mod _impl { } Ok(JSValue::UNDEFINED) } + } - /// RefCount destroy callback. Invoked when `ref_count` reaches zero. - /// Not `Drop` because this is an intrusive-refcounted `m_ctx` payload whose - /// box is freed here. - fn deinit(this: *mut Self) { - // SAFETY: called exactly once by IntrusiveRc when refcount hits 0; `this` - // is the heap::alloc pointer produced at construction. `this_value` - // (Strong) and `poll_ref` (CountedKeepAlive) are Drop types — freed by - // heap::take below. - unsafe { - (*this).stream.with_mut(|s| s.close()); - drop(bun_core::heap::take(this)); - } + // Called by RefCount when the count hits 0. `poll_ref`/`this_value` clean up + // via their own Drop impls; the Box free is the derive's default destructor. + // `Context` has no Drop, so the `close()` below is load-bearing. + impl Drop for NativeZlib { + fn drop(&mut self) { + self.stream.with_mut(|s| s.close()); } } diff --git a/src/runtime/server/AnyRequestContext.rs b/src/runtime/server/AnyRequestContext.rs index 088491a5fd89..b7c6cef9c9de 100644 --- a/src/runtime/server/AnyRequestContext.rs +++ b/src/runtime/server/AnyRequestContext.rs @@ -80,10 +80,12 @@ impl CtxKind } impl AnyRequestContext { - pub fn init(request_ctx: *const T) -> Self { + // Takes `*mut T`: callers write through `ptr` in `dispatch!`, so the + // pointer must carry mutable provenance. + pub fn init(request_ctx: *mut T) -> Self { Self { tag: T::TAG, - ptr: request_ctx as *mut (), + ptr: request_ctx.cast::<()>(), } } } @@ -222,14 +224,16 @@ impl AnyRequestContext { /// inside `NewServer` has a stable address, so deriving `&mut` here is /// sound as long as the caller upholds the usual single-writer rule on the /// JS thread. - pub fn dev_server_mut(self) -> Option<*mut crate::bake::DevServer::DevServer> { + pub fn dev_server_mut(self) -> Option> { dispatch!(self, None, |_T, ctx| { let server = ctx.server?.as_ptr(); // SAFETY: `ctx.server` is a non-null backref that outlives this context // and `dev_server` is a `Box` field never moved while requests are in // flight, so dereferencing for exclusive access on the JS thread is sound. let ds = unsafe { (*server).dev_server.as_deref_mut()? }; - Some(core::ptr::from_mut(ds)) + // SAFETY: the `Box` slot outlives every `AnyRequestContext`; + // `from_raw_mut` keeps the write provenance `assume_mut` needs. + Some(unsafe { bun_ptr::ParentRef::from_raw_mut(core::ptr::from_mut(ds)) }) }) } diff --git a/src/runtime/server/FileRoute.rs b/src/runtime/server/FileRoute.rs index 688cc7bcc1b1..bade2ef17de6 100644 --- a/src/runtime/server/FileRoute.rs +++ b/src/runtime/server/FileRoute.rs @@ -170,7 +170,7 @@ impl FileRoute { "expected blob not to be heap-allocated" ); *body_value = BodyValue::Blob(blob.dupe()); - let headers = headers_from(response.get_init_headers(), &blob); + let headers = headers_from(response.headers(), &blob); let status_code = response.status_code(); return Ok(Some(bun_core::heap::into_raw(Box::new(FileRoute { @@ -325,7 +325,7 @@ impl FileRoute { this.ref_(); if let Some(mut server) = this.server.get() { server.on_pending_request(); - resp.timeout(server.config().idle_timeout); + resp.timeout(server.config().idle_timeout.get()); } // Clone the path so the borrow into `this.blob.store` // doesn't span the scopeguard creation (the guard's closure may free @@ -552,7 +552,7 @@ impl FileRoute { pollable, offset: body_offset, length: body_len, - idle_timeout: this.server.get().unwrap().config().idle_timeout, + idle_timeout: this.server.get().unwrap().config().idle_timeout.get(), ctx: this_ptr.cast::(), on_complete: on_stream_complete, on_abort: None, diff --git a/src/runtime/server/HTMLBundle.rs b/src/runtime/server/HTMLBundle.rs index 45c3c8720afd..752a2a2ecd56 100644 --- a/src/runtime/server/HTMLBundle.rs +++ b/src/runtime/server/HTMLBundle.rs @@ -346,7 +346,7 @@ impl Route { method, resp, route: this, - is_response_pending: true, + is_response_pending: Cell::new(true), })); route.pending_responses.with_mut(|v| v.push(pending)); @@ -598,8 +598,7 @@ impl Route { // entry-point until after cloning so we retain the sole owner for // the `clone()` mutable borrow. Static routes are keyed by // `dest_path`, so registration order is immaterial. - let mut this_html_route: Option<(core::ptr::NonNull, Box<[u8]>)> = - None; + let mut this_html_route: Option<(Box, Box<[u8]>)> = None; // Create static routes for each output file // Index loop because the SourceMap branch reads a sibling entry. @@ -658,7 +657,7 @@ impl Route { } let cached_blob_size = blob.size() as u64; - let static_route = bun_core::heap::into_raw_nn(Box::new(StaticRoute { + let static_route = Box::new(StaticRoute { ref_count: Cell::new(1), blob, server: Cell::new(Some(server)), @@ -666,7 +665,7 @@ impl Route { headers, cached_blob_size, has_content_disposition: false, - })); + }); let mut route_path: &[u8] = &output_files[i].dest_path; // The route path gets cloned inside of appendStaticRoute. @@ -687,21 +686,18 @@ impl Route { bun_core::handle_oom(server.append_static_route( route_path, - AnyRoute::Static(static_route), + AnyRoute::Static(bun_core::heap::into_raw_nn(static_route)), MethodOptional::Any, )); } - let (html_route, html_route_path) = this_html_route.unwrap_or_else(|| { + let (mut html_route, html_route_path) = this_html_route.unwrap_or_else(|| { panic!("Internal assertion failure: HTML entry point not found in HTMLBundle.") }); - // SAFETY: html_route is a fresh heap::alloc with ref_count=1; - // sole owner before registration. - let html_route_clone = - bun_core::handle_oom(unsafe { &mut *html_route.as_ptr() }.clone(global_this)); + let html_route_clone = bun_core::handle_oom(html_route.clone(global_this)); bun_core::handle_oom(server.append_static_route( &html_route_path, - AnyRoute::Static(html_route), + AnyRoute::Static(bun_core::heap::into_raw_nn(html_route)), MethodOptional::Any, )); self.state.set(State::Html(html_route_clone)); @@ -726,20 +722,16 @@ impl Route { for pending_response_ptr in pending { // SAFETY: every entry was created via heap::alloc in on_any_request and // is removed exactly once (here, or via on_aborted which removes without freeing). - let pending_response = unsafe { &mut *pending_response_ptr }; - // `defer pending_response.deinit()` — heap::take + Drop at scope end. - let _drop = scopeguard::guard(pending_response_ptr, |p| { - // SAFETY: see above; reconstitutes the Box and runs `Drop`. - drop(unsafe { bun_core::heap::take(p) }); - }); + // Taking the Box back gives unique ownership; it runs `Drop` at scope end. + let pending_response = unsafe { bun_core::heap::take(pending_response_ptr) }; let resp = pending_response.resp; let method = pending_response.method; - if !pending_response.is_response_pending { + if !pending_response.is_response_pending.get() { // Aborted continue; } - pending_response.is_response_pending = false; + pending_response.is_response_pending.set(false); resp.clear_aborted(); match self.state.get() { @@ -793,7 +785,7 @@ impl Drop for Route { pub struct PendingResponse { method: Method, resp: AnyResponse, - is_response_pending: bool, + is_response_pending: Cell, // Raw ptr because the route owns the Vec containing this // PendingResponse; an `IntrusiveRc` field would form a cycle through // `Drop`. The ref is bumped/dropped manually via `RefCount::` calls. @@ -802,7 +794,7 @@ pub struct PendingResponse { impl Drop for PendingResponse { fn drop(&mut self) { - if self.is_response_pending { + if self.is_response_pending.get() { self.resp.clear_aborted(); self.resp.clear_on_writable(); self.resp.end_without_body(true); @@ -821,9 +813,9 @@ impl PendingResponse { /// (via `heap::take`) by this call. unsafe fn on_aborted(this: *mut PendingResponse, _resp: AnyResponse) { // SAFETY: caller contract. - let this_ref = unsafe { &mut *this }; - debug_assert!(this_ref.is_response_pending); - this_ref.is_response_pending = false; + let this_ref = unsafe { &*this }; + debug_assert!(this_ref.is_response_pending.get()); + this_ref.is_response_pending.set(false); // Technically, this could be the final ref count, but we don't want to risk it let route_ptr = this_ref.route; diff --git a/src/runtime/server/NodeHTTPResponse.rs b/src/runtime/server/NodeHTTPResponse.rs index 44467ad0e80d..3ed726e24878 100644 --- a/src/runtime/server/NodeHTTPResponse.rs +++ b/src/runtime/server/NodeHTTPResponse.rs @@ -385,10 +385,6 @@ impl NodeHTTPResponse { let Some(ws_handler) = server.web_socket_handler() else { return false; }; - // Lifetime-extend the handler past the method calls below. - // SAFETY: JS-thread only; the server (and its websocket config) outlives this call. - let ws_handler: &mut crate::server::WebSocketServerHandler = - unsafe { &mut *std::ptr::from_mut(ws_handler) }; let socket_value = self.get_server_socket_value(); if socket_value.is_empty() { return false; diff --git a/src/runtime/server/RequestContext.rs b/src/runtime/server/RequestContext.rs index 34e3589b2221..06b810397799 100644 --- a/src/runtime/server/RequestContext.rs +++ b/src/runtime/server/RequestContext.rs @@ -764,11 +764,7 @@ where if ctx.method == Method::HEAD { if let Some(resp) = ctx.resp { - let mut pair = HeaderResponsePair { - this: ctx, - response, - }; - resp.run_corked_with_type(Self::do_render_head_response, &raw mut pair); + resp.corked(|| Self::do_render_head_response(ctx, response)); } return; } @@ -1022,16 +1018,11 @@ where pub fn render_missing(&mut self) { if let Some(resp) = self.resp { - resp.run_corked_with_type(|ctx| Self::render_missing_corked(ctx), self); + resp.corked(|| Self::render_missing_corked(self)); } } - /// # Safety - /// `ctx` must point to a live `RequestContext` threaded through cork user-data. - pub(crate) fn render_missing_corked(ctx: *mut Self) { - // SAFETY: caller upholds the fn-level contract — `ctx` is the live - // `RequestContext` threaded through cork user-data. - let ctx = unsafe { &mut *ctx }; + pub(crate) fn render_missing_corked(ctx: &mut Self) { if let Some(resp) = ctx.resp { if !DEBUG_MODE { if !ctx.flags.has_written_status() { @@ -1859,7 +1850,7 @@ where // managing partial responses themselves. let user_handles_range = if let Some(r) = self.response_weakref.get() { r.status_code() != 200 - || r.get_init_headers_mut() + || r.headers() .map(|h| h.fast_has(jsc::HTTPHeaderName::ContentRange)) .unwrap_or(false) } else { @@ -1890,9 +1881,9 @@ where let mut crbuf = [0u8; RangeRequest::CONTENT_RANGE_BUF]; self.do_write_status(416); if let Some(response) = self.response_weakref.get() { - if let Some(mut headers_) = response.swap_init_headers() { - self.do_write_headers(&mut headers_); - // `HeadersRef` releases the +1 ref in Drop; do NOT + if let Some(headers_) = response.swap_init_headers() { + self.do_write_headers(&headers_); + // `FetchHeaders` releases the +1 ref in Drop; do NOT // call `.deref()` explicitly (would double-free). drop(headers_); } @@ -1962,7 +1953,7 @@ where } else { None }, - idle_timeout: server.config().idle_timeout, + idle_timeout: server.config().idle_timeout.get(), ctx: std::ptr::from_mut::(self).cast::(), on_complete: Self::on_file_stream_complete, on_abort: Some(Self::on_file_stream_abort), @@ -2015,22 +2006,16 @@ where /// JSSink is `repr(transparent)` so the inner-ptr free matches the /// outer allocation. fn destroy_sink(ptr: NonNull>) { - // `ptr` was `heap::alloc`'d in do_render_stream and is being consumed - // exactly once here. `JSSink` is repr(transparent), so the inner - // `HTTPServerWritable` shares the allocation Layout. - ResponseStream::::destroy( - ptr.as_ptr().cast::>(), - ); + // SAFETY: `ptr` was `heap::into_raw_nn`'d in do_render_stream and is + // consumed exactly once here. `JSSink` is repr(transparent), so the + // inner `HTTPServerWritable` shares the allocation Layout. + ResponseStream::::destroy(unsafe { + bun_core::heap::take(ptr.as_ptr().cast::>()) + }); } - fn do_render_stream(pair: *mut StreamPair<'_, ThisServer, SSL_ENABLED, DEBUG_MODE, HTTP3>) { + fn do_render_stream(this: &mut Self, stream: &mut WebCore::ReadableStream) { ctx_log!("doRenderStream"); - // SAFETY: pair is a stack local threaded through cork user-data. - let pair = unsafe { &mut *pair }; - // NOTE: reshaped for borrowck — split the two fields up front so - // `this` and `stream` are independent borrows of `*pair`. - let this: &mut Self = &mut *pair.this; - let stream = &mut pair.stream; debug_assert!(this.server.is_some()); // SAFETY: BACKREF let global_this = this.server().global_this(); @@ -2090,7 +2075,7 @@ where ResponseStreamJSSink::::assign_to_stream( global_this, stream.value, - &mut response_stream.sink, + std::ptr::from_mut(&mut response_stream.sink), signal_ptr_slot, ); @@ -2343,8 +2328,10 @@ where // we have to clone the request headers here since they will soon belong to a different request if !request_object.has_fetch_headers() { if !HTTP3 { - // `HeadersRef::create_from_uws` adopts the freshly-allocated +1 ref. - request_object.set_fetch_headers(Some(response::HeadersRef::create_from_uws(req))); + // `FetchHeaders::create_from_uws` adopts the freshly-allocated +1 ref. + // SAFETY: for !HTTP3, `req` is the live `uWS::HttpRequest*` for this callback frame. + let headers = unsafe { response::FetchHeaders::create_from_uws(req) }; + request_object.set_fetch_headers(Some(headers)); } } @@ -2426,20 +2413,12 @@ where || self.server().terminated() } - /// # Safety - /// `pair` must point to a live stack-local `HeaderResponseSizePair` threaded through cork user-data. - pub(crate) fn do_render_head_response_after_s3_size_resolved( - pair: *mut HeaderResponseSizePair<'_, ThisServer, SSL_ENABLED, DEBUG_MODE, HTTP3>, - ) { - // SAFETY: caller upholds the fn-level contract — `pair` points to a - // live stack-local `HeaderResponseSizePair` threaded through cork user-data. - let pair = unsafe { &mut *pair }; - let this = &mut *pair.this; + pub(crate) fn do_render_head_response_after_s3_size_resolved(this: &mut Self, size: usize) { this.render_metadata(); if let Some(resp) = this.resp { // SAFETY: FFI handle - resp.write_header_int(b"content-length", pair.size as u64); + resp.write_header_int(b"content-length", size as u64); } this.end_without_body(this.should_close_connection()); // `end_without_body` released the base ref; the caller @@ -2463,24 +2442,17 @@ where | S3::simple_request::S3StatResult::NotFound(_) => 0, S3::simple_request::S3StatResult::Success(stat) => stat.size, }; - let mut pair = HeaderResponseSizePair { this, size }; - resp.run_corked_with_type( - |p| Self::do_render_head_response_after_s3_size_resolved(p), - &raw mut pair, - ); + resp.corked(|| Self::do_render_head_response_after_s3_size_resolved(this, size)); } // No early returns above; explicit deref instead of a scopeguard that // would alias `&mut Self` through a captured raw pointer. this.deref(); } - fn do_render_head_response( - pair: *mut HeaderResponsePair<'_, ThisServer, SSL_ENABLED, DEBUG_MODE, HTTP3>, - ) { - // SAFETY: pair is a stack local threaded through cork user-data. - let pair = unsafe { &mut *pair }; - let this = &mut *pair.this; - let response_ptr = pair.response; + /// `response_ptr` is the JS wrapper's cell pointer, not a `&mut Response`: + /// it is handed to `set_response`, which stores it in a `WeakPtr` that + /// outlives any reborrow. The cell is GC-rooted by the calling frame. + fn do_render_head_response(this: &mut Self, response_ptr: *mut Response) { if this.resp.is_none() { return; } @@ -2499,7 +2471,7 @@ where // not derive framing from that body (or the user headers) either // (RFC 9110 §9.3.2): render the exact metadata+framing GET would. if HTTPStatusText::is_null_body(response.status_code()) { - Self::do_render_blob_corked(std::ptr::from_mut::(this)); + Self::do_render_blob_corked(this); return; } @@ -2525,11 +2497,8 @@ where Body::Value::Used | Body::Value::Null | Body::Value::Empty | Body::Value::Error(_) ) }; - // `fast_get`/`fast_has` take `&mut self` (FFI shim), so use the `_mut` - // accessor — `get_fetch_headers()` and `get_init_headers()` alias the - // same `init.headers` field. if !body_decides_framing { - if let Some(headers) = response.get_init_headers_mut() { + if let Some(headers) = response.headers() { // first respect the headers if !HTTP3 { if let Some(transfer_encoding) = @@ -2709,11 +2678,7 @@ where ctx.flags.set_response_protected(false); if ctx.method == Method::HEAD { if let Some(resp) = ctx.resp { - let mut pair = HeaderResponsePair { - this: ctx, - response, - }; - resp.run_corked_with_type(Self::do_render_head_response, &raw mut pair); + resp.corked(|| Self::do_render_head_response(ctx, response)); } return; } else { @@ -2787,11 +2752,7 @@ where ctx.flags.set_response_protected(false); if ctx.method == Method::HEAD { if let Some(resp) = ctx.resp { - let mut pair = HeaderResponsePair { - this: ctx, - response, - }; - resp.run_corked_with_type(Self::do_render_head_response, &raw mut pair); + resp.corked(|| Self::do_render_head_response(ctx, response)); } return; } @@ -3204,8 +3165,8 @@ where | readable_stream::Source::JavaScript | readable_stream::Source::Direct => { if let Some(resp) = this.resp { - let mut pair = StreamPair { stream, this }; - resp.run_corked_with_type(Self::do_render_stream, &raw mut pair); + let mut stream = stream; + resp.corked(|| Self::do_render_stream(this, &mut stream)); } return; } @@ -3337,19 +3298,14 @@ where // This is an important performance optimization if self.flags.has_abort_handler() && self.blob.fast_size() < 16384 - 1024 { if let Some(resp) = self.resp { - resp.run_corked_with_type(|ctx| Self::do_render_blob_corked(ctx), self); + resp.corked(|| Self::do_render_blob_corked(self)); } } else { - Self::do_render_blob_corked(std::ptr::from_mut::(self)); + Self::do_render_blob_corked(self); } } - /// # Safety - /// `this` must point to a live `RequestContext` threaded through cork user-data. - pub(crate) fn do_render_blob_corked(this: *mut Self) { - // SAFETY: caller upholds the fn-level contract — `this` points to a - // live `RequestContext` threaded through cork user-data. - let this = unsafe { &mut *this }; + pub(crate) fn do_render_blob_corked(this: &mut Self) { this.render_metadata(); this.render_bytes(); } @@ -3461,7 +3417,7 @@ where let mut exception_list_upstream: jsc::ExceptionList = Vec::new(); let prev_exception_list = vm.on_unhandled_rejection_exception_list; vm.on_unhandled_rejection_exception_list = - Some(NonNull::from(&mut exception_list_upstream)); + Some(bun_ptr::BackRef::new_mut(&mut exception_list_upstream)); (vm.on_unhandled_rejection)(vm, global_this, value); vm.on_unhandled_rejection_exception_list = prev_exception_list; @@ -3646,7 +3602,7 @@ where }; let (content_type, needs_content_type, content_type_needs_free) = - get_content_type(response.get_init_headers_mut(), &self.blob); + get_content_type(response.headers(), &self.blob); // NOTE: `MimeType` owns a `Cow<'static, [u8]>`; Drop handles the owned case. // Hold the value past all reads below, then let it drop at scope end. let _ct_guard = scopeguard::guard(content_type_needs_free, |_needs| { @@ -3655,7 +3611,7 @@ where }); let mut has_content_disposition = false; let mut has_content_range = false; - if let Some(mut headers_) = response.swap_init_headers() { + if let Some(headers_) = response.swap_init_headers() { has_content_disposition = headers_.fast_has(jsc::HTTPHeaderName::ContentDisposition); has_content_range = headers_.fast_has(jsc::HTTPHeaderName::ContentRange); // For .slice()-driven ranges, only promote to 206 if the user @@ -3668,11 +3624,8 @@ where } self.do_write_status(status); - self.do_write_headers(&mut headers_); - // `HeadersRef` is RAII — its Drop - // already calls `WebCore__FetchHeaders__deref`, so an explicit - // `.deref()` here would resolve (via DerefMut) to the inherent - // `FetchHeaders::deref` and double-free the C++ object. + self.do_write_headers(&headers_); + // Release the +1 before the body is written. drop(headers_); } else if needs_content_range { status = 206; @@ -3794,7 +3747,7 @@ where } } - fn do_write_headers(&mut self, headers: &mut FetchHeaders) { + fn do_write_headers(&mut self, headers: &FetchHeaders) { ctx_log!("writeHeaders"); headers.fast_remove(jsc::HTTPHeaderName::ContentLength); headers.fast_remove(jsc::HTTPHeaderName::TransferEncoding); @@ -3806,7 +3759,8 @@ where headers.fast_remove(jsc::HTTPHeaderName::Upgrade); } if let Some(resp) = self.resp { - headers.to_uws_response(Self::RESP_KIND, any_response_as_ptr(resp)); + // SAFETY: `resp` is the live `uWS::HttpResponse*`/`Http3Response*` matching `RESP_KIND`. + unsafe { headers.to_uws_response(Self::RESP_KIND, any_response_as_ptr(resp)) }; } } @@ -4318,25 +4272,6 @@ request_ctx_exports! { Bun__HTTPRequestContextDebugH3__onRejectStream; } -pub struct StreamPair<'a, ThisServer, const SSL: bool, const DBG: bool, const H3: bool> { - pub this: &'a mut RequestContext, - pub stream: WebCore::ReadableStream, -} - -pub struct HeaderResponseSizePair<'a, ThisServer, const SSL: bool, const DBG: bool, const H3: bool> -{ - pub this: &'a mut RequestContext, - pub size: usize, -} - -pub struct HeaderResponsePair<'a, ThisServer, const SSL: bool, const DBG: bool, const H3: bool> { - pub this: &'a mut RequestContext, - /// The JS wrapper's cell pointer, not a `&mut Response`: the receiving - /// frame hands it to `set_response`, which stores it in a `WeakPtr` that - /// outlives any reborrow. The cell is GC-rooted by the constructing frame. - pub response: *mut Response, -} - pub struct PathnameFormatter<'a, ThisServer, const SSL: bool, const DBG: bool, const H3: bool> { ctx: &'a RequestContext, } @@ -4528,7 +4463,7 @@ impl Flags { } } -fn get_content_type(headers: Option<&mut FetchHeaders>, blob: &AnyBlob) -> (MimeType, bool, bool) { +fn get_content_type(headers: Option<&FetchHeaders>, blob: &AnyBlob) -> (MimeType, bool, bool) { let mut needs_content_type = true; let mut content_type_needs_free = false; diff --git a/src/runtime/server/ServerConfig.rs b/src/runtime/server/ServerConfig.rs index 95d4ccf20d11..f27a88342d32 100644 --- a/src/runtime/server/ServerConfig.rs +++ b/src/runtime/server/ServerConfig.rs @@ -22,7 +22,7 @@ use crate::socket::ssl_config::SSLConfigFromJs; pub struct ServerConfig { pub address: Address, - pub idle_timeout: u8, // TODO: should we match websocket default idleTimeout of 120? + pub idle_timeout: core::cell::Cell, // TODO: should we match websocket default idleTimeout of 120? pub has_idle_timeout: bool, // TODO: use webkit URL parser instead of bun's // NOTE: only the owned buffer is stored; callers parse on @@ -72,7 +72,7 @@ impl Default for ServerConfig { fn default() -> Self { Self { address: Address::default(), - idle_timeout: 10, + idle_timeout: core::cell::Cell::new(10), has_idle_timeout: false, base_uri: Box::default(), ssl_config: None, @@ -263,7 +263,7 @@ impl ServerConfig { // residual `self` is a no-op. let mut that = ServerConfig { address: core::mem::take(&mut self.address), - idle_timeout: self.idle_timeout, + idle_timeout: core::cell::Cell::new(self.idle_timeout.get()), has_idle_timeout: self.has_idle_timeout, base_uri: core::mem::take(&mut self.base_uri), ssl_config: self.ssl_config.take(), @@ -1136,7 +1136,7 @@ impl ServerConfig { ))); } - args.idle_timeout = idle_timeout as u8; + args.idle_timeout.set(idle_timeout as u8); } } diff --git a/src/runtime/server/StaticRoute.rs b/src/runtime/server/StaticRoute.rs index 7e03b63b7d49..8615fff46b54 100644 --- a/src/runtime/server/StaticRoute.rs +++ b/src/runtime/server/StaticRoute.rs @@ -215,7 +215,7 @@ impl StaticRoute { let mut has_content_disposition = false; - if let Some(h) = response.get_init_headers_mut() { + if let Some(h) = response.headers() { has_content_disposition = h.fast_has(HTTPHeaderName::ContentDisposition); h.fast_remove(HTTPHeaderName::TransferEncoding); h.fast_remove(HTTPHeaderName::ContentLength); @@ -234,7 +234,7 @@ impl StaticRoute { } let mut headers: Headers = bun_http_jsc::headers_jsc::from_fetch_headers( - response.get_init_headers(), + response.headers(), any_blob_content_type(&blob), ); @@ -289,7 +289,7 @@ impl StaticRoute { (*this).ref_(); if let Some(mut server) = (*this).server.get() { server.on_pending_request(); - resp.timeout(server.config().idle_timeout); + resp.timeout(server.config().idle_timeout.get()); } resp.corked(|| (*this).render_metadata_and_end(resp)); Self::on_response_complete(this, resp); @@ -356,7 +356,7 @@ impl StaticRoute { (*this).ref_(); if let Some(mut server) = (*this).server.get() { server.on_pending_request(); - resp.timeout(server.config().idle_timeout); + resp.timeout(server.config().idle_timeout.get()); } let mut finished = false; (*this).do_render_blob(resp, &mut finished); @@ -446,7 +446,7 @@ impl StaticRoute { // SAFETY: caller contract. unsafe { if let Some(server) = (*this).server.get() { - resp.timeout(server.config().idle_timeout); + resp.timeout(server.config().idle_timeout.get()); } if !(*this).on_writable_bytes(write_offset, resp) { @@ -575,7 +575,7 @@ impl StaticRoute { (*this).ref_(); if let Some(mut server) = (*this).server.get() { server.on_pending_request(); - resp.timeout(server.config().idle_timeout); + resp.timeout(server.config().idle_timeout.get()); } (*this).do_write_status(304, resp); (*this).do_write_headers(resp); diff --git a/src/runtime/server/mod.rs b/src/runtime/server/mod.rs index 1b0f1fb48c43..caa8b17cadab 100644 --- a/src/runtime/server/mod.rs +++ b/src/runtime/server/mod.rs @@ -237,7 +237,7 @@ pub struct NewServer { /// Cached `h3=":"; ma=86400` for Alt-Svc on H1 responses; formatted /// once in onH3Listen so renderMetadata doesn't reformat per-request. pub h3_alt_svc: Box<[u8]>, - pub js_value: jsc::JsRef, + pub js_value: jsc::JsCell, /// Potentially null before listen() is called, and once .destroy() is called. // LIFETIMES.tsv = STATIC → `&'static VirtualMachine`. `BackRef` for safe // `Deref` while keeping the struct `'static` (process-lifetime VM). @@ -261,7 +261,7 @@ pub struct NewServer { pub method_name_cache: [core::cell::Cell; N_HTTP_METHODS], pub base_url_string_for_joining: Box<[u8]>, pub config: ServerConfig, - pub pending_requests: usize, + pub pending_requests: core::cell::Cell, pub request_pool: *mut request_context::RequestContextStackAllocator, /// Null until the H3 listen path runs (`HAS_H3 && config.http3`); never /// allocated when `!SSL`. Kept as a raw nullable pointer rather than a @@ -288,7 +288,7 @@ pub struct NewServer { /// times due to SNI, so we have to store them. pub user_routes: Vec>, - pub on_clienterror: jsc::StrongOptional, + pub on_clienterror: jsc::JsCell, pub inspector_server_id: jsc::DebuggerId, } @@ -502,8 +502,8 @@ impl NewServer { Some(&self.h3_alt_svc) } - pub fn on_pending_request(&mut self) { - self.pending_requests += 1; + pub fn on_pending_request(&self) { + self.pending_requests.set(self.pending_requests.get() + 1); } /// Build the server's base URL string (`http(s)://host:port/`, or a @@ -558,8 +558,8 @@ impl NewServer { /// alive (callers must only use this while the server JS object is kept /// alive elsewhere). pub fn js_value_assert_alive(&self) -> JSValue { - debug_assert!(self.js_value.is_not_empty()); - self.js_value.try_get().expect("js_value alive") + debug_assert!(self.js_value.get().is_not_empty()); + self.js_value.get().try_get().expect("js_value alive") } /// Per-monomorphization static. @@ -616,10 +616,9 @@ impl NewServer { method: Option, ) -> Option> { jsc::mark_binding!(); - // SAFETY: `this`/`resp` are live for the duration of the uWS callback; - // re-borrowed disjointly below to avoid stacking `&mut` across the - // `ctx.create()` call (which stores `this` as a backref). - let server = unsafe { &mut *this }; + // SAFETY: `this` is live for the duration of the uWS callback. A shared + // borrow suffices: `on_pending_request` mutates through a `Cell`. + let server = unsafe { &*this }; // S008: `Response` is a ZST opaque — safe `*mut → &mut` deref. let resp_ref = bun_opaque::opaque_deref_mut(resp); @@ -662,7 +661,7 @@ impl NewServer { server.on_pending_request(); req.set_yield(false); - resp_ref.timeout(server.config.idle_timeout); + resp_ref.timeout(server.config.idle_timeout.get()); // Since we do timeouts by default, we should tell the user when // this happens - but limit it to only warn once. @@ -1157,10 +1156,14 @@ impl NewServer { ); let vm = this_ref.vm_mut(); req.set_yield(false); - resp.timeout(this_ref.config.idle_timeout); + resp.timeout(this_ref.config.idle_timeout.get()); let global = this_ref.global_this(); - let this_object = this_ref.js_value.try_get().unwrap_or(JSValue::UNDEFINED); + let this_object = this_ref + .js_value + .get() + .try_get() + .unwrap_or(JSValue::UNDEFINED); // Compute the JS method-name string up front so the FFI closure // doesn't need to reborrow `req` (it's already `&mut`-borrowed below). @@ -1221,6 +1224,11 @@ impl NewServer { }) .unwrap_or_else(|err| global.take_exception(err)); + // SAFETY: the out-param carries the +1 written by C++; released below, + // or handed to the promise continuation when the handler went async. + let nhr_ref: Option> = (!node_http_response.is_null()) + .then(|| unsafe { bun_ptr::RefPtr::take_ref(node_http_response) }); + enum HttpResult { Rejection(JSValue), Exception(JSValue), @@ -1258,10 +1266,7 @@ impl NewServer { } jsc::js_promise::Status::Pending => { global.handle_rejected_promises(); - if !node_http_response.is_null() { - // SAFETY: out-param written by `on_request_ffi`; - // owned ref held until `deref()` below. - let nhr = unsafe { &mut *node_http_response }; + if let Some(nhr) = nhr_ref.as_deref() { // Single `Cell` load for all three flag checks (no // re-entry between them). let nhr_flags = nhr.flags.get(); @@ -1305,16 +1310,13 @@ impl NewServer { match &http_result { HttpResult::Exception(err) | HttpResult::Rejection(err) => { - // SAFETY: `vm` is the process-static VirtualMachine. - let _ = unsafe { &mut *vm }.uncaught_exception( + let _ = jsc::VirtualMachine::get_mut().uncaught_exception( global, *err, matches!(http_result, HttpResult::Rejection(_)), ); - if !node_http_response.is_null() { - // SAFETY: see `nhr` above. - let nhr = unsafe { &mut *node_http_response }; + if let Some(nhr) = nhr_ref.as_deref() { let nhr_flags = nhr.flags.get(); if !nhr_flags.contains(NhrFlags::UPGRADED) { if let Some(raw) = nhr.raw_response.get() { @@ -1344,9 +1346,7 @@ impl NewServer { HttpResult::Success | HttpResult::Pending => {} } - if !node_http_response.is_null() { - // SAFETY: see `nhr` above. - let nhr = unsafe { &mut *node_http_response }; + if let Some(nhr) = nhr_ref.as_deref() { let nhr_flags = nhr.flags.get(); if !nhr_flags.contains(NhrFlags::UPGRADED) { if let Some(raw) = nhr.raw_response.get() { @@ -1358,8 +1358,7 @@ impl NewServer { // If we ended the response without attaching an ondata handler, we discard the body read stream else if !matches!(http_result, HttpResult::Pending) { let this_value = nhr.get_this_value(); - // SAFETY: `vm` is the process-static VirtualMachine. - nhr.maybe_stop_reading_body(unsafe { &mut *vm }, this_value); + nhr.maybe_stop_reading_body(jsc::VirtualMachine::get_mut(), this_value); } } } else if nhr_flags.contains(NhrFlags::IS_REQUEST_PENDING) { @@ -1382,9 +1381,13 @@ impl NewServer { // SAFETY: `vm` is the process-static VirtualMachine. unsafe { (*vm).drain_microtasks() }; } - if !is_async && !node_http_response.is_null() { - // SAFETY: out-param ref taken in C++; synchronous path drops it. - unsafe { &mut *node_http_response }.deref(); + if let Some(nhr) = nhr_ref { + // Async handlers transfer the C++ ref to the promise continuation. + if is_async { + let _ = nhr.leak(); + } else { + nhr.deref(); + } } } @@ -1427,7 +1430,7 @@ impl NewServer { // same crate, separate file, alongside `on_reload`/`reload_static_routes`. pub fn on_static_request_complete(&mut self) { - self.pending_requests -= 1; + self.pending_requests.set(self.pending_requests.get() - 1); self.deinit_if_we_can(); } @@ -1437,7 +1440,7 @@ impl NewServer { // for the server's lifetime); `.event_loop()` returns the VM-owned // `*mut EventLoop`. Single-threaded JS context, no aliasing `&mut`. unsafe { (*(*self.vm_mut()).event_loop()).process_gc_timer() }; - self.pending_requests -= 1; + self.pending_requests.set(self.pending_requests.get() - 1); self.deinit_if_we_can(); } @@ -1456,11 +1459,11 @@ impl NewServer { self.listener.is_some() || (Self::HAS_H3 && self.h3_listener.is_some()) } - pub fn set_idle_timeout(&mut self, seconds: core::ffi::c_uint) { - self.config.idle_timeout = seconds.min(255) as u8; + pub fn set_idle_timeout(&self, seconds: core::ffi::c_uint) { + self.config.idle_timeout.set(seconds.min(255) as u8); } - pub fn set_flags(&mut self, require_host_header: bool, use_strict_method_validation: bool) { + pub fn set_flags(&self, require_host_header: bool, use_strict_method_validation: bool) { if let Some(app) = self.app { // S012: `NewApp` is a ZST opaque — safe `*mut → &mut` deref. bun_opaque::opaque_deref_mut(app) @@ -1468,7 +1471,7 @@ impl NewServer { } } - pub fn set_max_http_header_size(&mut self, max_header_size: u64) { + pub fn set_max_http_header_size(&self, max_header_size: u64) { if let Some(app) = self.app { // S012: `NewApp` is a ZST opaque — safe `*mut → &mut` deref. bun_opaque::opaque_deref_mut(app).set_max_http_header_size(max_header_size); @@ -1563,8 +1566,8 @@ impl NewServer { } pub fn stop(&mut self, abrupt: bool) { - if self.js_value.is_not_empty() { - self.js_value.downgrade(); + if self.js_value.get().is_not_empty() { + self.js_value.with_mut(|r| r.downgrade()); } if self.config.allow_hot && !self.config.id.is_empty() { // `hot_map()` is reached via the thread-local VM singleton (raw ptr @@ -1586,7 +1589,7 @@ impl NewServer { pub fn deinit_if_we_can(&mut self) { httplog!( "deinitIfWeCan. requests={}, listener={}, websockets={}, has_handled_all_closed_promise={}, all_closed_promise={}, has_js_deinited={}", - self.pending_requests, + self.pending_requests.get(), if self.listener.is_none() { "null" } else { @@ -1604,10 +1607,10 @@ impl NewServer { } else { "no" }, - matches!(self.js_value, jsc::JsRef::Finalized), + matches!(self.js_value.get(), jsc::JsRef::Finalized), ); - if self.pending_requests == 0 + if self.pending_requests.get() == 0 && !self.has_listener() && !self.has_active_web_sockets() && !self @@ -1643,7 +1646,10 @@ impl NewServer { vm_ref, ); } - if self.pending_requests == 0 && !self.has_listener() && !self.has_active_web_sockets() { + if self.pending_requests.get() == 0 + && !self.has_listener() + && !self.has_active_web_sockets() + { if let Some(ws) = self.config.websocket.as_mut() { ws.handler.app = None; } @@ -1661,7 +1667,7 @@ impl NewServer { } // Only free the memory if the JS reference has been freed too. - if matches!(self.js_value, jsc::JsRef::Finalized) { + if matches!(self.js_value.get(), jsc::JsRef::Finalized) { self.schedule_deinit(); } } @@ -1675,9 +1681,7 @@ impl NewServer { self.flags.insert(ServerFlags::DEINIT_SCHEDULED); httplog!("scheduleDeinit"); - // SAFETY: `vm_mut()` is the process-static `*mut VirtualMachine` (non-null - // for the server's lifetime); single-threaded JS context, no aliasing `&mut`. - let vm = unsafe { &mut *self.vm_mut() }; + let vm = jsc::VirtualMachine::get_mut(); if !self.flags.contains(ServerFlags::TERMINATED) { // App.close can cause finalizers to run. @@ -1697,7 +1701,7 @@ impl NewServer { |this| { // SAFETY: `this` is the unique owning server pointer enqueued // above; the task runs once on the JS thread. - Self::deinit(this); + Self::deinit(unsafe { bun_core::heap::take(this) }); Ok(()) }, )); @@ -1710,7 +1714,10 @@ impl NewServer { self.listener = Some(socket); // SAFETY: `vm_mut()` is the process-static `*mut VirtualMachine` (non-null // for the server's lifetime); single-threaded JS context. - unsafe { (*self.vm_mut()).event_loop_handle = Some(bun_io::Loop::get()) }; + unsafe { + (*self.vm_mut()).event_loop_handle = + Some(bun_ptr::BackRef::from_raw(bun_io::Loop::get())) + }; if !SSL { // S008: `app::ListenSocket` is a ZST opaque — safe deref. let fd = bun_opaque::opaque_deref_mut(socket).socket().fd(); @@ -1825,43 +1832,34 @@ impl NewServer { // ─── deinit ────────────────────────────────────────────────────────────── /// Tear down the uws app handles and free the boxed server. Only called /// from `schedule_deinit`'s task or synchronously on listen-failure. - /// - /// # Safety - /// `this` must be the unique owning pointer to a heap-allocated `NewServer` - /// produced by [`Self::init`]; no other reference may be live, and `this` - /// must not be used after this returns. - pub(crate) fn deinit(this: *mut Self) { + // `boxed_local`: the `Box` is the ownership unit being reclaimed here. + #[allow(clippy::boxed_local)] + pub(crate) fn deinit(mut self: Box) { httplog!("deinit"); - // SAFETY: `this` was heap-allocated in `init()` and is uniquely owned here. - let this_ref = unsafe { &mut *this }; // This should've already been handled in stop_listening; however, when // the JS VM terminates, it hypothetically might not call stop_listening. - this_ref.notify_inspector_server_stopped(); - if this_ref.vm().test_isolation_enabled { + self.notify_inspector_server_stopped(); + if self.vm().test_isolation_enabled { if let Some(handles) = crate::jsc_hooks::isolation_handles() { - handles.swap_remove(&crate::jsc_hooks::IsolationHandle::Server(AnyServer::from( - this.cast_const(), - ))); + let packed = AnyServer::from(std::ptr::from_ref::(&*self)); + handles.swap_remove(&crate::jsc_hooks::IsolationHandle::Server(packed)); } } // owned-field cleanup (all_closed_promise / user_routes / // config / on_clienterror / h3_alt_svc / dev_server / plugins) is - // handled by the heap::take drop below — see `impl Drop for NewServer`. + // handled by the `Box` drop at end of scope — see `impl Drop for NewServer`. if Self::HAS_H3 { - if let Some(h3a) = this_ref.h3_app.take() { + if let Some(h3a) = self.h3_app.take() { // SAFETY: live H3::App handle owned by this server. unsafe { uws_sys::h3::App::destroy(h3a) }; } } - if let Some(app) = this_ref.app.take() { + if let Some(app) = self.app.take() { // SAFETY: live uws App handle owned by this server. unsafe { uws_sys::NewApp::::destroy(app) }; } - - // SAFETY: paired with heap::alloc in `init()`. - drop(unsafe { bun_core::heap::take(this) }); } pub fn set_using_custom_expect_handler(&mut self, value: bool) { @@ -1897,8 +1895,8 @@ impl NewServer { h3_app: None, h3_listener: None, h3_alt_svc: Box::<[u8]>::default(), - js_value: jsc::JsRef::empty(), - pending_requests: 0, + js_value: jsc::JsCell::new(jsc::JsRef::empty()), + pending_requests: core::cell::Cell::new(0), request_pool: >::request_pool(), // Plain HTTP servers never allocate the ~816 KB H3 pool; defer to // the H3-listen path (`listen()` below) so HTTPS servers that @@ -1913,7 +1911,7 @@ impl NewServer { flags: ServerFlags::default(), plugins: None, user_routes: Vec::new(), - on_clienterror: jsc::StrongOptional::empty(), + on_clienterror: jsc::JsCell::new(jsc::StrongOptional::empty()), inspector_server_id: jsc::DebuggerId::init(0), })); @@ -2331,11 +2329,10 @@ impl NewServer { if let Some(dev) = dev_server { // dev.setRoutes might register its own "/*" HTTP handler // SAFETY: `dev` is the live `*mut DevServer` snapshotted from - // `self.dev_server` above; `self_ptr` is the live server. The two - // allocations are disjoint so the `&mut` borrows do not alias. - has_dev_server_for_star_path = bun_core::handle_oom( - unsafe { &mut *dev }.set_routes::(unsafe { &mut *self_ptr }), - ); + // `self.dev_server` above; it points into the boxed `DevServer`, a + // separate allocation from `*self`, so the `&mut`s do not alias. + has_dev_server_for_star_path = + bun_core::handle_oom(unsafe { &mut *dev }.set_routes::(self)); if has_dev_server_for_star_path { // Assume dev server "/*" covers all methods if it exists star_methods_covered_by_user = http_method::Set::all(); @@ -2493,8 +2490,8 @@ impl NewServer { let _ = global.throw(format_args!( "Failed to create HTTPS server: missing tls config" )); - // SAFETY: caller contract — `this` is the live boxed server from `init()`. - Self::deinit(this); + // SAFETY: `this` is the live boxed server from `init()`, uniquely owned here. + Self::deinit(unsafe { bun_core::heap::take(this) }); return JSValue::ZERO; }; @@ -2506,8 +2503,8 @@ impl NewServer { } // SAFETY: `this` is the live boxed server from `init()`; no other borrow is live. unsafe { (*this).app = None }; - // SAFETY: caller contract — `this` is the live boxed server from `init()`. - Self::deinit(this); + // SAFETY: `this` is the live boxed server from `init()`, uniquely owned here. + Self::deinit(unsafe { bun_core::heap::take(this) }); return JSValue::ZERO; } }; @@ -2515,15 +2512,15 @@ impl NewServer { unsafe { (*this).app = Some(app) }; if Self::HAS_H3 && this_ref.config.http3 { - let idle_timeout = this_ref.config.idle_timeout as u32; + let idle_timeout = this_ref.config.idle_timeout.get() as u32; let h3 = match uws_sys::h3::App::create(&ssl_options, idle_timeout) { Some(a) => Some(a), None => { if !global.has_exception() { let _ = global.throw(format_args!("Failed to create HTTP/3 server")); } - // SAFETY: caller contract — `this` is the live boxed server from `init()`. - Self::deinit(this); + // SAFETY: `this` is the live boxed server from `init()`, uniquely owned here. + Self::deinit(unsafe { bun_core::heap::take(this) }); return JSValue::ZERO; } }; @@ -2565,13 +2562,13 @@ impl NewServer { bstr::BStr::new(server_name.to_bytes()) )); } - // SAFETY: caller contract — `this` is the live boxed server from `init()`. - Self::deinit(this); + // SAFETY: `this` is the live boxed server from `init()`, uniquely owned here. + Self::deinit(unsafe { bun_core::heap::take(this) }); return JSValue::ZERO; } if throw_ssl_error_if_necessary(global) { - // SAFETY: caller contract — `this` is the live boxed server from `init()`. - Self::deinit(this); + // SAFETY: `this` is the live boxed server from `init()`, uniquely owned here. + Self::deinit(unsafe { bun_core::heap::take(this) }); return JSValue::ZERO; } @@ -2580,8 +2577,8 @@ impl NewServer { // S012: `NewApp` is a ZST opaque — safe `*mut → &mut` deref. bun_opaque::opaque_deref_mut(app).domain(z); if throw_ssl_error_if_necessary(global) { - // SAFETY: caller contract — `this` is the live boxed server from `init()`. - Self::deinit(this); + // SAFETY: `this` is the live boxed server from `init()`, uniquely owned here. + Self::deinit(unsafe { bun_core::heap::take(this) }); return JSValue::ZERO; } @@ -2630,8 +2627,8 @@ impl NewServer { bstr::BStr::new(sni_name.to_bytes()) )); } - // SAFETY: caller contract — `this` is the live boxed server from `init()`. - Self::deinit(this); + // SAFETY: `this` is the live boxed server from `init()`, uniquely owned here. + Self::deinit(unsafe { bun_core::heap::take(this) }); return JSValue::ZERO; } } @@ -2647,15 +2644,15 @@ impl NewServer { bstr::BStr::new(sni_name.to_bytes()) )); } - // SAFETY: caller contract — `this` is the live boxed server from `init()`. - Self::deinit(this); + // SAFETY: `this` is the live boxed server from `init()`, uniquely owned here. + Self::deinit(unsafe { bun_core::heap::take(this) }); return JSValue::ZERO; } // S012: `NewApp` is a ZST opaque — safe `*mut → &mut` deref. bun_opaque::opaque_deref_mut(app).domain(z); if throw_ssl_error_if_necessary(global) { - // SAFETY: caller contract — `this` is the live boxed server from `init()`. - Self::deinit(this); + // SAFETY: `this` is the live boxed server from `init()`, uniquely owned here. + Self::deinit(unsafe { bun_core::heap::take(this) }); return JSValue::ZERO; } // SAFETY: `this` is the live boxed server from `init()`; no other borrow is live. @@ -2669,8 +2666,8 @@ impl NewServer { if !global.has_exception() { let _ = global.throw(format_args!("Failed to create HTTP server")); } - // SAFETY: caller contract — `this` is the live boxed server from `init()`. - Self::deinit(this); + // SAFETY: `this` is the live boxed server from `init()`, uniquely owned here. + Self::deinit(unsafe { bun_core::heap::take(this) }); return JSValue::ZERO; } }; @@ -2810,8 +2807,11 @@ impl NewServer { } if !this_ref.config.http1 { // SAFETY: per-thread VM singleton; no aliasing `&mut`. + // SAFETY: the uws loop is process-static and outlives the VM. jsc::VirtualMachine::get().as_mut().event_loop_handle = - Some(bun_io::Loop::get()); + Some(unsafe { + bun_ptr::BackRef::from_raw(bun_io::Loop::get()) + }); } } } @@ -2847,8 +2847,8 @@ impl NewServer { } if global.has_exception() { - // SAFETY: caller contract — `this` is the live boxed server from `init()`. - Self::deinit(this); + // SAFETY: `this` is the live boxed server from `init()`, uniquely owned here. + Self::deinit(unsafe { bun_core::heap::take(this) }); return JSValue::ZERO; } @@ -3187,7 +3187,7 @@ impl ServerLike for NewServer { } #[inline(always)] fn js_value(&self) -> &jsc::JsRef { - &self.js_value + self.js_value.get() } #[inline] fn h3_alt_svc(&self) -> Option<&[u8]> { @@ -3485,7 +3485,7 @@ impl AnyServer { } pub fn on_pending_request(&mut self) { - any_server_dispatch_mut!(self, |s| s.on_pending_request()) + any_server_dispatch!(self, |s| s.on_pending_request()) } /// Dispatch the user `fetch` handler: diff --git a/src/runtime/server/server_body.rs b/src/runtime/server/server_body.rs index ebb8da341881..7ca2ac160970 100644 --- a/src/runtime/server/server_body.rs +++ b/src/runtime/server/server_body.rs @@ -12,10 +12,8 @@ use crate::node::types::PathLikeExt as _; use crate::webcore::BlobExt; use crate::webcore::body::Value as BodyValue; use crate::webcore::fetch as Fetch; -use crate::webcore::response::HeadersRef; -use crate::webcore::{ - self as WebCore, AbortSignal, AnyBlob, Blob, FetchHeaders, Request, Response, -}; +use crate::webcore::response::FetchHeaders; +use crate::webcore::{self as WebCore, AbortSignal, AnyBlob, Blob, Request, Response}; use ::bstr::BStr; use bun_collections::HashMap; use bun_core::{Output, fmt as bun_fmt}; @@ -561,20 +559,13 @@ impl AnyRoute { let Some(headers_js) = argument.get(init_ctx.global, b"headers")? else { return Ok(None); }; + // Owns the `+1` from `create_from_js`; `Drop` releases it at scope end. let fetch_headers = FetchHeaders::create_from_js(init_ctx.global, headers_js)?; - let _fh_guard = scopeguard::guard(fetch_headers, |fh| { - // S008: `FetchHeaders` is an `opaque_ffi!` ZST — safe deref. - if let Some(h) = fh { - bun_opaque::opaque_deref_mut(h.as_ptr()).deref(); - } - }); if init_ctx.global.has_exception() { return Err(JsError::Thrown); } - // S008: `FetchHeaders` is an `opaque_ffi!` ZST — safe deref. - let headers_ref = fetch_headers.map(|p| bun_opaque::opaque_deref(p.as_ptr().cast_const())); - let route = Self::from_options(init_ctx.global, headers_ref, &mut path)?; + let route = Self::from_options(init_ctx.global, fetch_headers.as_ref(), &mut path)?; if is_index_route { return Ok(Some(route)); @@ -1214,9 +1205,13 @@ pub(super) fn on_reject_impl(global: &JSGlobalObject, callframe: &CallFrame) -> Ok(JSValue::UNDEFINED) } +/// Borrows the `FetchHeaders` owned by a JS `Headers` wrapper. Takes no ref. #[inline] -fn fetch_headers_from_js(value: JSValue, global: &JSGlobalObject) -> Option<*mut FetchHeaders> { - FetchHeaders::cast_(value, global.vm()).map(|p| p.as_ptr()) +fn fetch_headers_from_js( + value: JSValue, + global: &JSGlobalObject, +) -> Option> { + FetchHeaders::cast_(value, global.vm()) } /// Per-process latch for the dev-mode idle-timeout warning. The @@ -1290,7 +1285,7 @@ impl<'a, Ctx: RequestCtxOps> PreparedRequestFor<'a, Ctx> { SavedRequest { js_request: StrongOptional::create(self.js_request, global), request: self.request_object, - ctx: AnyRequestContext::init(std::ptr::from_ref::(self.ctx)), + ctx: AnyRequestContext::init(std::ptr::from_mut::(self.ctx)), response: RespLike::to_any_response(resp), } } @@ -1351,7 +1346,7 @@ impl NewServer { /// `&mut` accessor for the live uws App. Only call from paths where the /// server is running (`self.app` set in `listen()`). #[inline] - fn app_mut(&self) -> &mut uws_sys::NewApp { + fn app_mut(&mut self) -> &mut uws_sys::NewApp { // S008: `NewApp` is a ZST opaque — safe `*mut → &mut` deref via // const-asserted `bun_opaque::opaque_deref_mut`. `self.app` is `Some` // for the lifetime of any JS-reachable `Server` (set in `listen()`, @@ -1627,13 +1622,10 @@ where } let value = seconds.to_u32(); - if let Some(request) = ::from_js(arguments[0]) { - // SAFETY: from_js returns a live *mut Request - let _ = unsafe { &mut *request }.request_context.set_timeout(value); - } else if let Some(response) = ::from_js(arguments[0]) - { - // SAFETY: from_js returns a live *mut NodeHTTPResponse - unsafe { &mut *response }.set_timeout((value % 255) as u8); + if let Some(request) = arguments[0].as_class_ref::() { + let _ = request.request_context.set_timeout(value); + } else if let Some(response) = arguments[0].as_class_ref::() { + response.set_timeout((value % 255) as u8); } else { return Err(self .global() @@ -1745,9 +1737,7 @@ where return Ok(JSValue::FALSE); } - if let Some(node_http_response) = ::from_js(object) { - // SAFETY: from_js returns a live *mut NodeHTTPResponse - let node_http_response = unsafe { &mut *node_http_response }; + if let Some(node_http_response) = object.as_class_ref::() { if node_http_response .flags .get() @@ -1762,15 +1752,8 @@ where let mut data_value = JSValue::ZERO; - // if we converted a HeadersInit to a Headers object, we need to free it - let fetch_headers_to_deref: core::cell::Cell> = - core::cell::Cell::new(None); - let _fh_guard = scopeguard::guard(&fetch_headers_to_deref, |cell| { - if let Some(fh) = cell.get() { - // S008: `FetchHeaders` is an `opaque_ffi!` ZST — safe deref. - bun_opaque::opaque_deref_mut(fh).deref(); - } - }); + // if we converted a HeadersInit to a Headers object, `Drop` frees it + let mut created_fetch_headers: Option = None; let mut sec_websocket_protocol = ZigString::EMPTY; let mut sec_websocket_extensions = ZigString::EMPTY; @@ -1807,30 +1790,27 @@ where break 'getter; } - let fetch_headers_to_use: *mut FetchHeaders = - match fetch_headers_from_js(headers_value, global) { - Some(h) => h, - None => 'brk: { - if headers_value.is_object() { - if let Some(fetch_headers) = - FetchHeaders::create_from_js(global, headers_value)? - { - fetch_headers_to_deref - .set(Some(fetch_headers.as_ptr())); - break 'brk fetch_headers.as_ptr(); - } - } - if !global.has_exception() { - return Err(global.throw_invalid_arguments(format_args!( - "upgrade options.headers must be a Headers or an object" - ))); + let borrowed_fetch_headers = fetch_headers_from_js(headers_value, global); + let fetch_headers_to_use: &FetchHeaders = match borrowed_fetch_headers + .as_deref() + { + Some(h) => h, + None => 'brk: { + if headers_value.is_object() { + if let Some(fetch_headers) = + FetchHeaders::create_from_js(global, headers_value)? + { + break 'brk &*created_fetch_headers.insert(fetch_headers); } - return Err(JsError::Thrown); } - }; - // S008: `FetchHeaders` is an `opaque_ffi!` ZST — safe deref. - let fetch_headers_to_use = - bun_opaque::opaque_deref_mut(fetch_headers_to_use); + if !global.has_exception() { + return Err(global.throw_invalid_arguments(format_args!( + "upgrade options.headers must be a Headers or an object" + ))); + } + return Err(JsError::Thrown); + } + }; if global.has_exception() { return Err(JsError::Thrown); @@ -1861,10 +1841,14 @@ where if let Some(raw_response) = node_http_response.raw_response.get() { // we must write the status first so that 200 OK isn't written raw_response.write_status(b"101 Switching Protocols"); - fetch_headers_to_use.to_uws_response( - ResponseKind::from(SSL, false), - raw_response.socket().cast::(), - ); + // SAFETY: `raw_response.socket()` is the live + // `uWS::HttpResponse` for this request. + unsafe { + fetch_headers_to_use.to_uws_response( + ResponseKind::from(SSL, false), + raw_response.socket().cast::(), + ); + } } } @@ -1930,15 +1914,8 @@ where let mut _sec_websocket_protocol_owned = bun_core::ZigStringSlice::empty(); let mut _sec_websocket_extensions_owned = bun_core::ZigStringSlice::empty(); - // NOTE: `FetchHeaders::fast_get` takes `&mut self` (FFI signature - // is `*mut`), so go through the `BodyMixin` accessor which yields a - // `NonNull` instead of the inherent `&FetchHeaders` getter. if let Some(head) = crate::webcore::body::BodyMixin::get_fetch_headers(request) { use jsc::HTTPHeaderName; - // `head` is a live, intrusively-refcounted C++ handle owned by - // `request.headers`. `FetchHeaders` is an opaque ZST FFI handle - // (S008) — safe `*mut → &mut` via `opaque_deref_mut`. - let head = bun_opaque::opaque_deref_mut(head.as_ptr()); if let Some(key) = head.fast_get(HTTPHeaderName::SecWebSocketKey) { _sec_websocket_key_owned = key.to_slice_clone(); sec_websocket_key_str = ZigString::init(_sec_websocket_key_owned.slice()); @@ -1953,8 +1930,6 @@ where } } - // SAFETY: upgrader_ptr is live (ref_() above) - let upgrader = unsafe { &mut *upgrader_ptr }; if let Some(req_ptr) = upgrader.req { // NOTE: `RequestContext.req` is type-erased to `*mut c_void` // (RequestContext.rs:82). `server.upgrade()` is HTTP/1-only — H3 @@ -1989,15 +1964,12 @@ where } let mut data_value = JSValue::ZERO; - // Non-unit guard state: holds the temporarily-created FetchHeaders (if - // any) and derefs it on scope exit. Populated below via DerefMut. - let mut fetch_headers_to_deref = scopeguard::guard(None::<*mut FetchHeaders>, |fh| { - // S008: `FetchHeaders` is an `opaque_ffi!` ZST — safe deref. - if let Some(h) = fh { - bun_opaque::opaque_deref_mut(h).deref() - } - }); - let mut fetch_headers_to_use: Option<*mut FetchHeaders> = None; + // Holds the temporarily-created FetchHeaders (if any); `Drop` derefs it. + let mut created_fetch_headers: Option = None; + // The JS `Headers` wrapper owns this ref (see `fetch_headers_from_js`); + // the binding only has to outlive `fetch_headers_to_use`'s borrow of it. + let borrowed_fetch_headers: Option>; + let mut fetch_headers_to_use: Option<&FetchHeaders> = None; if let Some(opts) = optional { 'getter: { @@ -2021,15 +1993,15 @@ where break 'getter; } use jsc::HTTPHeaderName; - let fh: *mut FetchHeaders = match fetch_headers_from_js(headers_value, global) { + borrowed_fetch_headers = fetch_headers_from_js(headers_value, global); + let fh: &FetchHeaders = match borrowed_fetch_headers.as_deref() { Some(h) => h, None => { if headers_value.is_object() { if let Some(created) = FetchHeaders::create_from_js(global, headers_value)? { - *fetch_headers_to_deref = Some(created.as_ptr()); - created.as_ptr() + &*created_fetch_headers.insert(created) } else if !global.has_exception() { return Err(global.throw_invalid_arguments(format_args!( "upgrade options.headers must be a Headers or an object" @@ -2051,8 +2023,6 @@ where return Err(JsError::Thrown); } - // S008: `FetchHeaders` is an `opaque_ffi!` ZST — safe deref. - let fh = bun_opaque::opaque_deref_mut(fh); if let Some(p) = fh.fast_get(HTTPHeaderName::SecWebSocketProtocol) { _sec_websocket_protocol_owned = p.to_slice_clone(); sec_websocket_protocol = @@ -2087,11 +2057,14 @@ where if fetch_headers_to_use.is_some() || cookies_to_write.is_some() { resp.write_status(b"101 Switching Protocols"); if let Some(h) = fetch_headers_to_use { - // S008: `FetchHeaders` is an `opaque_ffi!` ZST — safe deref. - bun_opaque::opaque_deref_mut(h).to_uws_response( - ResponseKind::from(SSL, false), - resp.socket().cast::(), - ); + // SAFETY: `resp.socket()` is the live `uWS::HttpResponse` for + // this request. + unsafe { + h.to_uws_response( + ResponseKind::from(SSL, false), + resp.socket().cast::(), + ); + } } if let Some(c) = cookies_to_write.as_mut() { c.write( @@ -2226,7 +2199,7 @@ where let route_list_value = self.set_routes(); if new_config.had_routes_object { - if let Some(server_js_value) = self.js_value.try_get() { + if let Some(server_js_value) = self.js_value.get().try_get() { if !server_js_value.is_empty() { Self::js_gc_route_list_set(server_js_value, global, route_list_value); } @@ -2257,7 +2230,7 @@ where } let route_list_value = self.set_routes(); if !route_list_value.is_empty() { - if let Some(server_js_value) = self.js_value.try_get() { + if let Some(server_js_value) = self.js_value.get().try_get() { if !server_js_value.is_empty() { Self::js_gc_route_list_set(server_js_value, &self.global(), route_list_value); } @@ -2296,7 +2269,7 @@ where self.on_reload_from_zig(&mut new_config, global); - Ok(self.js_value.try_get().unwrap_or(JSValue::UNDEFINED)) + Ok(self.js_value.get().try_get().unwrap_or(JSValue::UNDEFINED)) } #[bun_jsc::host_fn(method)] @@ -2325,7 +2298,7 @@ where ); } - let mut headers: Option = None; + let mut headers: Option = None; let mut method = Method::GET; // SAFETY: bun_vm() returns the live per-thread VM singleton. let mut args = jsc::ArgumentsSlice::init(ctx.bun_vm(), arguments); @@ -2373,22 +2346,13 @@ where if let Some(headers_) = opts.fast_get(ctx, jsc::BuiltinName::Headers)? { if let Some(headers__) = FetchHeaders::cast_(headers_, ctx.vm()) { - // NOTE: `cast_` returns the `FetchHeaders*` held by the - // JS `Headers` wrapper (`JSFetchHeaders`'s internal - // `Ref`) without bumping the refcount — - // the FFI surface has `WebCore__FetchHeaders__deref` but - // no `ref()`, so a +1 cannot be taken here. Adopting - // hands that wrapper-held ref to the constructed - // `Request` (via `Request::init2` below): the eventual - // single deref happens when the Request's finalizer - // drops its `headers` field (`HeadersRef::Drop`, - // Response.rs), pairing with the wrapper's +1. - // SAFETY: `headers__` is live (rooted by `headers_`), - // and ownership of one ref transfers as described above. - headers = Some(unsafe { HeadersRef::adopt(headers__) }); + // The JS `Headers` wrapper owns that ref, so the Request + // gets its own copy — the same `clone_this` a + // `new Response(_, { headers })` takes in `Response::init`. + headers = headers__.clone_this(ctx)?; } else if let Some(headers__) = FetchHeaders::create_from_js(ctx, headers_)? { - // SAFETY: create_from_js returns a +1 ref. - headers = Some(unsafe { HeadersRef::adopt(headers__) }); + // `create_from_js` already hands back the owned `+1`. + headers = Some(headers__); } } @@ -2550,7 +2514,7 @@ where #[bun_jsc::host_fn(getter)] pub fn get_pending_requests(&self, _: &JSGlobalObject) -> JSValue { - JSValue::js_number((self.pending_requests as u32 & 0x7FFF_FFFF) as i32 as f64) + JSValue::js_number((self.pending_requests.get() as u32 & 0x7FFF_FFFF) as i32 as f64) } #[bun_jsc::host_fn(getter)] @@ -2683,12 +2647,12 @@ where // `deinit_if_we_can` may defer the actual free (pending requests still // hold a ref), so hand ownership back to the raw teardown path. let this = bun_core::heap::release(self); - this.js_value.finalize(); + this.js_value.with_mut(|r| r.finalize()); this.deinit_if_we_can(); } pub fn get_all_closed_promise(&mut self, global: &JSGlobalObject) -> JSValue { - if !self.has_listener() && self.pending_requests == 0 { + if !self.has_listener() && self.pending_requests.get() == 0 { return JSPromise::resolved_promise(global, JSValue::UNDEFINED).to_js(); } let prom = &mut self.all_closed_promise; @@ -2762,7 +2726,7 @@ where req.set_yield(true); return; } - self.pending_requests += 1; + self.pending_requests.set(self.pending_requests.get() + 1); req.set_yield(false); let buffer_writer = bun_js_printer::BufferWriter::init(); @@ -2785,7 +2749,7 @@ where resp.write_header_int(b"Age", 0); let buffer = writer.ctx.written(); resp.end(buffer, false); - self.pending_requests -= 1; + self.pending_requests.set(self.pending_requests.get() - 1); } // `on_chrome_dev_tools_json_request` is defined once below (next to @@ -2976,7 +2940,7 @@ where self.on_pending_request(); ReqLike::set_yield(req, false); - RespLike::timeout(resp, self.config.idle_timeout); + RespLike::timeout(resp, self.config.idle_timeout.get()); // Since we do timeouts by default, we should tell the user when // this happens - but limit it to only warn once. @@ -3060,7 +3024,7 @@ where let signal_for_req = unsafe { jsc::AbortSignalRef::adopt((*signal).ref_()) }; let request_object_box = Request::new(Request::init( ctx.ctx_method(), - AnyRequestContext::init(std::ptr::from_ref::(ctx)), + AnyRequestContext::init(std::ptr::from_mut::(ctx)), SSL, Some(signal_for_req), body_hive, @@ -3080,11 +3044,9 @@ where // the rest of the pipeline never needs to know which transport // delivered the bytes. if Ctx::IS_H3 { - // SAFETY: create_from_h3 returns a +1-ref FetchHeaders; adopt into RAII wrapper. + // SAFETY: `req` is the live `uWS::Http3Request` for this request. request_object.set_fetch_headers(Some(unsafe { - crate::webcore::response::HeadersRef::adopt(FetchHeaders::create_from_h3( - std::ptr::from_mut(req).cast::(), - )) + FetchHeaders::create_from_h3(std::ptr::from_mut(req).cast::()) })); // NOTE: `ReqLike::{url,header}` both borrow `&mut req`; the // returned slices alias the same uWS-owned header buffer. Format @@ -3273,7 +3235,7 @@ where resp.end_without_body(true); return; } - this.pending_requests += 1; + this.pending_requests.set(this.pending_requests.get() + 1); req.set_yield(false); // SAFETY: `request_pool` is non-null while the server is alive; `claim()` // reserves a fresh slot whose `Drop` releases it on panic before init. @@ -3309,7 +3271,7 @@ where let signal_for_req = unsafe { jsc::AbortSignalRef::adopt((*signal).ref_()) }; let request_object_box = Request::new(Request::init( ctx.method, - AnyRequestContext::init(std::ptr::from_ref(ctx)), + AnyRequestContext::init(std::ptr::from_mut(ctx)), SSL, Some(signal_for_req), body_hive, @@ -3484,12 +3446,12 @@ where } pub fn on_client_error_callback( - &mut self, + &self, socket: &mut uws::Socket, error_code: u8, raw_packet: &[u8], ) { - let Some(callback) = self.on_clienterror.get() else { + let Some(callback) = self.on_clienterror.get().get() else { return; }; { @@ -3593,18 +3555,14 @@ pub(super) fn server_set_idle_timeout_( ))); } let value = seconds.to_u32(); - if let Some(this) = server.as_::() { - // SAFETY: `as_` returned a non-null `*mut` to a live JS-wrapped server. - unsafe { &mut *this }.set_idle_timeout(value); - } else if let Some(this) = server.as_::() { - // SAFETY: `as_` returned a non-null `*mut` to a live JS-wrapped server. - unsafe { &mut *this }.set_idle_timeout(value); - } else if let Some(this) = server.as_::() { - // SAFETY: `as_` returned a non-null `*mut` to a live JS-wrapped server. - unsafe { &mut *this }.set_idle_timeout(value); - } else if let Some(this) = server.as_::() { - // SAFETY: `as_` returned a non-null `*mut` to a live JS-wrapped server. - unsafe { &mut *this }.set_idle_timeout(value); + if let Some(this) = server.as_class_ref::() { + this.set_idle_timeout(value); + } else if let Some(this) = server.as_class_ref::() { + this.set_idle_timeout(value); + } else if let Some(this) = server.as_class_ref::() { + this.set_idle_timeout(value); + } else if let Some(this) = server.as_class_ref::() { + this.set_idle_timeout(value); } else { return Err(global.throw(format_args!( "Failed to set timeout: The 'this' value is not a Server." @@ -3632,12 +3590,11 @@ pub(super) fn server_set_on_client_error_( macro_rules! handle { ($T:ty) => { - if let Some(this) = server.as_::<$T>() { - // SAFETY: as_ returned a non-null *mut to a live server. - let this = unsafe { &mut *this }; + if let Some(this) = server.as_class_ref::<$T>() { if let Some(app) = this.app { - this.on_clienterror.deinit(); - this.on_clienterror = StrongOptional::create(callback, global); + this.on_clienterror.with_mut(|c| c.deinit()); + this.on_clienterror + .set(StrongOptional::create(callback, global)); // uws_sys::App::on_client_error takes the raw C-ABI handler shape; // wrap our typed callback in an extern "C" thunk that slices raw_packet. extern "C" fn thunk( @@ -3648,9 +3605,9 @@ pub(super) fn server_set_on_client_error_( raw_packet: *mut u8, raw_packet_len: c_int, ) { - // SAFETY: user_data is the `*mut Self` registered below; socket is a live + // SAFETY: user_data is the `$T` registered below; socket is a live // uWS socket; raw_packet/raw_packet_len describe a valid (possibly empty) buffer. - let this = unsafe { &mut *user_data.cast::<$T>() }; + let this = unsafe { &*user_data.cast::<$T>() }; let packet: &[u8] = if raw_packet_len > 0 { // SAFETY: uWS guarantees `raw_packet` points to `raw_packet_len` // readable bytes when `raw_packet_len > 0`. @@ -3662,7 +3619,7 @@ pub(super) fn server_set_on_client_error_( this.on_client_error_callback(bun_opaque::opaque_deref_mut(socket), error_code, packet); } // S008: `NewApp` is a ZST opaque — safe `*mut → &mut` deref. - bun_opaque::opaque_deref_mut(app).on_client_error(thunk, core::ptr::from_mut::<$T>(this).cast::()); + bun_opaque::opaque_deref_mut(app).on_client_error(thunk, core::ptr::from_ref::<$T>(this).cast::().cast_mut()); } return Ok(JSValue::UNDEFINED); } @@ -3688,18 +3645,14 @@ pub(super) fn server_set_app_flags_( ))); } - if let Some(this) = server.as_::() { - // SAFETY: `as_` returned a non-null `*mut` to a live JS-wrapped server. - unsafe { &mut *this }.set_flags(require_host_header, use_strict_method_validation); - } else if let Some(this) = server.as_::() { - // SAFETY: `as_` returned a non-null `*mut` to a live JS-wrapped server. - unsafe { &mut *this }.set_flags(require_host_header, use_strict_method_validation); - } else if let Some(this) = server.as_::() { - // SAFETY: `as_` returned a non-null `*mut` to a live JS-wrapped server. - unsafe { &mut *this }.set_flags(require_host_header, use_strict_method_validation); - } else if let Some(this) = server.as_::() { - // SAFETY: `as_` returned a non-null `*mut` to a live JS-wrapped server. - unsafe { &mut *this }.set_flags(require_host_header, use_strict_method_validation); + if let Some(this) = server.as_class_ref::() { + this.set_flags(require_host_header, use_strict_method_validation); + } else if let Some(this) = server.as_class_ref::() { + this.set_flags(require_host_header, use_strict_method_validation); + } else if let Some(this) = server.as_class_ref::() { + this.set_flags(require_host_header, use_strict_method_validation); + } else if let Some(this) = server.as_class_ref::() { + this.set_flags(require_host_header, use_strict_method_validation); } else { return Err(global.throw(format_args!( "Failed to set timeout: The 'this' value is not a Server." @@ -3719,18 +3672,14 @@ pub(super) fn server_set_max_http_header_size_( ))); } - if let Some(this) = server.as_::() { - // SAFETY: `as_` returned a non-null `*mut` to a live JS-wrapped server. - unsafe { &mut *this }.set_max_http_header_size(max_header_size); - } else if let Some(this) = server.as_::() { - // SAFETY: `as_` returned a non-null `*mut` to a live JS-wrapped server. - unsafe { &mut *this }.set_max_http_header_size(max_header_size); - } else if let Some(this) = server.as_::() { - // SAFETY: `as_` returned a non-null `*mut` to a live JS-wrapped server. - unsafe { &mut *this }.set_max_http_header_size(max_header_size); - } else if let Some(this) = server.as_::() { - // SAFETY: `as_` returned a non-null `*mut` to a live JS-wrapped server. - unsafe { &mut *this }.set_max_http_header_size(max_header_size); + if let Some(this) = server.as_class_ref::() { + this.set_max_http_header_size(max_header_size); + } else if let Some(this) = server.as_class_ref::() { + this.set_max_http_header_size(max_header_size); + } else if let Some(this) = server.as_class_ref::() { + this.set_max_http_header_size(max_header_size); + } else if let Some(this) = server.as_class_ref::() { + this.set_max_http_header_size(max_header_size); } else { return Err(global.throw(format_args!( "Failed to set maxHeaderSize: The 'this' value is not a Server." diff --git a/src/runtime/shell/Builtin.rs b/src/runtime/shell/Builtin.rs index 983e0a83b166..eb02cbd1b086 100644 --- a/src/runtime/shell/Builtin.rs +++ b/src/runtime/shell/Builtin.rs @@ -376,15 +376,14 @@ impl BuiltinIO { // and preserved across `dup_ref` so `2>&1` lands in stdout's // bytelist. // SAFETY: caller contract — shell env outlives the Cmd node - // (single-threaded); `captured` points into a live - // `ShellExecEnv` Bufio. - unsafe { - let captured = match *target { + // (single-threaded). + let captured = unsafe { + match *target { IoKind::Stdout => (*shell).buffered_stdout(), IoKind::Stderr | IoKind::Stdin => (*shell).buffered_stderr(), - }; - (*captured).append_slice(buf) + } }; + captured.with_mut(|captured| captured.append_slice(buf)); Ok(buf.len()) } BuiltinIO::ArrayBuf { buf: arraybuf, i } => { @@ -747,10 +746,9 @@ impl Builtin { } else if let Some(body) = crate::webcore::body::Value::from_request_or_response(jsval) { - // SAFETY: returned a live JSC-owned `*mut Value` borrowed - // from a Response/Request wrapper. - let body = unsafe { &mut *body }; - let is_file_blob = matches!(body, crate::webcore::body::Value::Blob(b) + // Shared reborrow ends here, before the `throw` below; the + // `&mut` for `use_()` is minted fresh afterwards. + let is_file_blob = matches!(&*body, crate::webcore::body::Value::Blob(b) if !b.needs_to_read_file()); if (redirect.stdout() || redirect.stderr()) && !is_file_blob { let _ = global.throw(format_args!( @@ -846,16 +844,12 @@ impl Builtin { // No-IO path: append to the shell env's captured stderr and finish // synchronously with exit 1 (Cmd::on_io_writer_chunk's behaviour). if let OutKind::Pipe = &interp.as_cmd(cmd).io.stderr { - // SAFETY: single trampoline frame; no other borrow of the env's - // (or its parent's) stderr buffer is live. - let stderr = unsafe { - interp - .as_cmd_mut(cmd) - .base - .shell_mut() - .buffered_stderr_mut() - }; - stderr.append_slice(&buf); + interp + .as_cmd(cmd) + .base + .shell() + .buffered_stderr() + .with_mut(|stderr| stderr.append_slice(&buf)); } let parent = interp.as_cmd(cmd).base.parent; interp.child_done(parent, cmd, 1) diff --git a/src/runtime/shell/IOReader.rs b/src/runtime/shell/IOReader.rs index ef0b3dd38767..f1e6eb8d4afa 100644 --- a/src/runtime/shell/IOReader.rs +++ b/src/runtime/shell/IOReader.rs @@ -2,10 +2,10 @@ //! //! *NOTE* This type is reference counted via `Arc`; see the `Drop` impl note. -use core::cell::UnsafeCell; #[cfg(not(windows))] use core::ffi::c_void; +use bun_jsc::JsCell; use bun_sys::{self as sys, Fd}; use crate::shell::interpreter::{EventLoopHandle, Interpreter, NodeId}; @@ -62,12 +62,15 @@ struct State { } pub struct IOReader { - /// Split out of `State` so `state()`'s `&mut State` never overlaps the + /// Split out of `State` so a `&mut State` never overlaps the /// `&mut ReaderImpl` the read-loop caller holds while invoking vtable /// callbacks (see `BufferedReaderParent` aliasing contract). Both cells /// root at SharedReadWrite; callbacks touch only `state` fields. - reader: UnsafeCell, - state: UnsafeCell, + /// + /// MUST NOT be touched from a `BufferedReaderParent` vtable callback: the + /// read loop holds a live `&mut ReaderImpl` on its stack for the duration. + reader: JsCell, + state: JsCell, } // SAFETY: shell is single-threaded; `Arc` is used purely for refcounting. @@ -92,36 +95,13 @@ impl IOReader { } impl IOReader { - #[inline] - #[allow(clippy::mut_from_ref)] // interior mutability via UnsafeCell; single-threaded - fn state(&self) -> &mut State { - // SAFETY: shell is single-threaded; no overlapping borrow of `state` - // escapes a callback (see struct doc comment). - unsafe { &mut *self.state.get() } - } - - #[inline] - #[allow(clippy::mut_from_ref)] // interior mutability via UnsafeCell; single-threaded - fn reader(&self) -> &mut ReaderImpl { - // SAFETY: single-threaded. Split into its own cell so a `&mut ReaderImpl` - // held by the bun_io read loop never overlaps a `&mut State` derived in a - // vtable callback (see struct doc comment). - // - // MUST NOT be invoked from within a `BufferedReaderParent` vtable - // callback (`on_read_chunk_cb`/`on_reader_done_cb`/`on_reader_error`): - // the read loop already holds a live `&mut ReaderImpl` on its stack - // while the callback runs (PipeReader.rs aliasing contract), so - // re-deriving here would create two simultaneous `&mut` to the same - // BufferedReader = Stacked-Borrows UB. - unsafe { &mut *self.reader.get() } - } - /// Bump our own Arc strong count. Held across re-entrant `run_yield` calls /// whose child callback may drop the last external ref and free us /// mid-method. #[inline] fn keepalive(&self) -> std::sync::Arc { - self.state() + self.state + .get() .self_weak .upgrade() .expect("IOReader::keepalive after last Arc dropped") @@ -140,8 +120,8 @@ impl IOReader { reader.source = Some(bun_io::Source::File(bun_io::Source::open_file(fd))); } let this = std::sync::Arc::new_cyclic(|w| IOReader { - reader: UnsafeCell::new(reader), - state: UnsafeCell::new(State { + reader: JsCell::new(reader), + state: JsCell::new(State { fd, buf: Vec::new(), readers: Readers::new(), @@ -158,13 +138,11 @@ impl IOReader { // NOTE: set the parent backref after Arc allocation so the // address is stable. let parent: *const IOReader = std::sync::Arc::as_ptr(&this); - // SAFETY: `Arc::as_ptr` yields `*const IOReader`, but every field of - // `IOReader` is `UnsafeCell`, so all mutation flows through interior - // mutability (SharedReadWrite). The `*mut` cast exists solely to satisfy - // `set_parent`'s `*mut` signature for the vtable backref; the - // `BufferedReaderParent` callbacks only ever reborrow it as `&Self` to - // call `&self` methods — no `&mut IOReader` is materialized from it. - unsafe { (*this.reader.get()).set_parent(parent.cast_mut().cast()) }; + // Every field is a `JsCell`, so mutation flows through interior + // mutability; the `*mut` cast only satisfies `set_parent`'s signature — + // callbacks reborrow it as `&Self`, never as `&mut IOReader`. + this.reader + .with_mut(|r| r.set_parent(parent.cast_mut().cast())); crate::shell_log!("IOReader(0x{:x}, fd={}) create", parent as usize, fd); this } @@ -179,21 +157,22 @@ impl IOReader { #[allow(clippy::not_unsafe_ptr_arg_deref)] pub fn set_interp(&self, interp: *mut Interpreter) { // SAFETY: precondition above. - self.state().interp = unsafe { bun_ptr::ParentRef::from_nullable_mut(interp) }; + let interp = unsafe { bun_ptr::ParentRef::from_nullable_mut(interp) }; + self.state.with_mut(|s| s.interp = interp); } #[inline] pub fn fd(&self) -> Fd { - self.state().fd + self.state.get().fd } #[inline] pub fn evtloop(&self) -> EventLoopHandle { - self.state().evtloop + self.state.get().evtloop } pub fn memory_cost(&self) -> usize { - let s = self.state(); + let s = self.state.get(); core::mem::size_of::() + s.buf.capacity() + s.readers.capacity() * core::mem::size_of::() @@ -207,7 +186,7 @@ impl IOReader { fn io_evtloop(&self) -> bun_io::EventLoopHandle { // SAFETY: `bun_io::EventLoopHandle` stores `*mut c_void` purely for // type-erasure; vtable consumers treat the pointee as read-only - self.state().evtloop.as_event_loop_ctx() + self.state.get().evtloop.as_event_loop_ctx() } /// Only does things on windows. @@ -215,25 +194,27 @@ impl IOReader { fn set_reading(&self, reading: bool) { #[cfg(windows)] { - self.state().is_reading = reading; + self.state.with_mut(|s| s.is_reading = reading); } let _ = reading; } /// Idempotent function to start the reading. pub fn start(&self) -> Yield { - self.state().started = true; + self.state.with_mut(|s| s.started = true); #[cfg(not(windows))] { - let r = self.reader(); - let need_start = match &r.handle { + let need_start = match &self.reader.get().handle { bun_io::pipes::PollOrFd::Closed => true, bun_io::pipes::PollOrFd::Poll(p) => !p.is_registered(), bun_io::pipes::PollOrFd::Fd(_) => true, }; if need_start { - let fd = self.state().fd; - if let Err(e) = r.start(fd, true) { + let fd = self.state.get().fd; + // `start` needs `&mut ReaderImpl`; end that borrow before + // `on_reader_error` re-enters the interpreter. + let res = self.reader.with_mut(|r| r.start(fd, true)); + if let Err(e) = res { self.on_reader_error(&e); } } @@ -241,12 +222,12 @@ impl IOReader { } #[cfg(windows)] { - let s = self.state(); - if s.is_reading { + if self.state.get().is_reading { return Yield::suspended(); } - s.is_reading = true; - if let Err(e) = self.reader().start_with_current_pipe() { + self.state.with_mut(|s| s.is_reading = true); + let res = self.reader.with_mut(|r| r.start_with_current_pipe()); + if let Err(e) = res { self.on_reader_error(&e); return Yield::failed(); } @@ -256,18 +237,20 @@ impl IOReader { /// Only adds if not already present. pub fn add_reader(&self, reader: ChildPtr) { - let s = self.state(); - if !s.readers.contains(&reader) { - s.readers.push(reader); - } + self.state.with_mut(|s| { + if !s.readers.contains(&reader) { + s.readers.push(reader); + } + }); } /// Unregister a listener; no-op if it was never added. pub fn remove_reader(&self, reader: ChildPtr) { - let s = self.state(); - if let Some(idx) = s.readers.iter().position(|r| *r == reader) { - s.readers.swap_remove(idx); - } + self.state.with_mut(|s| { + if let Some(idx) = s.readers.iter().position(|r| *r == reader) { + s.readers.swap_remove(idx); + } + }); } /// The `BufferedReader.onReadChunk` hook. @@ -284,20 +267,20 @@ impl IOReader { // `&mut State` across the dispatch. Re-derive `state()` per access // instead. let mut i = 0usize; - while i < self.state().readers.len() { - let r = self.state().readers[i]; - let interp = self.state().interp; + while i < self.state.get().readers.len() { + let r = self.state.get().readers[i]; + let interp = self.state.get().interp; let mut remove = false; self.run_yield(dispatch_read_chunk(r, chunk, &mut remove, interp)); if remove { - self.state().readers.swap_remove(i); + self.state.with_mut(|s| s.readers.swap_remove(i)); } else { i += 1; } } let should_continue = has_more != bun_io::ReadState::Eof; - if should_continue && !self.state().readers.is_empty() { + if should_continue && !self.state.get().readers.is_empty() { self.set_reading(true); // NOTE: no explicit re-arm (`registerPoll()` on posix / // `startWithCurrentPipe()` on windows) here: that would re-derive @@ -323,12 +306,13 @@ impl IOReader { // alive across the loop. let _keepalive = self.keepalive(); self.set_reading(false); - let s = self.state(); - s.err = Some(err.to_shell_system_error()); - s.raw_err = Some(err.clone()); - // NOTE: reshaped for borrowck — copy out before dispatching. - let readers: Vec = s.readers.clone(); - let interp = s.interp; + // NOTE: copy out and end the borrow before dispatching — the callee + // may re-enter `add_reader`/`remove_reader`. + let (readers, interp): (Vec, _) = self.state.with_mut(|s| { + s.err = Some(err.to_shell_system_error()); + s.raw_err = Some(err.clone()); + (s.readers.clone(), s.interp) + }); for r in readers { // Re-derive a fresh SystemError per callee (see // IOWriter.on_error note). @@ -344,13 +328,13 @@ impl IOReader { // Hold a strong ref across the body. let _keepalive = self.keepalive(); self.set_reading(false); - let s = self.state(); - let readers: Vec = s.readers.clone(); - let interp = s.interp; // `SystemError` isn't `Clone` yet, so we keep the source `sys::Error` // (which IS `Clone`) and re-derive a fresh `SystemError` per callee — - // same approach as `on_reader_error`. - let raw_err = s.raw_err.clone(); + // same approach as `on_reader_error`. Copy out before dispatching. + let (readers, interp, raw_err): (Vec, _, _) = { + let s = self.state.get(); + (s.readers.clone(), s.interp, s.raw_err.clone()) + }; for r in readers { let ee = raw_err.as_ref().map(|e| e.to_shell_system_error()); self.run_yield(dispatch_reader_done(r, ee, interp)); @@ -358,7 +342,7 @@ impl IOReader { } fn run_yield(&self, y: Yield) { - let Some(interp) = self.state().interp else { + let Some(interp) = self.state.get().interp else { debug_assert!( matches!(y, Yield::Done | Yield::Suspended), "IOReader async callback fired without interp backref" @@ -402,28 +386,29 @@ impl Drop for IOReader { // TODO: revisit if a child callback can drop the last Arc while // BufferedReader is still on the stack — would need the // EventLoopTask hop once the shell EventLoopHandle shim is real. - let s = self.state.get_mut(); - let r = self.reader.get_mut(); - if s.fd != Fd::INVALID { - #[cfg(windows)] - { - // windows reader closes the file descriptor - if r.source.is_some() && !r.source.as_ref().is_some_and(|src| src.is_closed()) { - r.close_impl::(); + let fd = self.state.get().fd; + self.reader.with_mut(|r| { + if fd != Fd::INVALID { + #[cfg(windows)] + { + // windows reader closes the file descriptor + if r.source.is_some() && !r.source.as_ref().is_some_and(|src| src.is_closed()) { + r.close_impl::(); + } } - } - #[cfg(not(windows))] - { - // We cleared CLOSE_HANDLE in init(), so reader Drop will not - // return the FilePoll to its pool. Do it explicitly (without - // closing the fd — we own that and close it ourselves below). - if matches!(r.handle, bun_io::pipes::PollOrFd::Poll(_)) { - r.handle.close_impl(None, None::, false); + #[cfg(not(windows))] + { + // We cleared CLOSE_HANDLE in init(), so reader Drop will not + // return the FilePoll to its pool. Do it explicitly (without + // closing the fd — we own that and close it ourselves below). + if matches!(r.handle, bun_io::pipes::PollOrFd::Poll(_)) { + r.handle.close_impl(None, None::, false); + } + let _ = sys::close(fd); } - let _ = sys::close(s.fd); } - } - r.disable_keeping_process_alive(()); + r.disable_keeping_process_alive(()); + }); // `reader` Drop handles its own deinit. } } diff --git a/src/runtime/shell/IOWriter.rs b/src/runtime/shell/IOWriter.rs index c9b3b44bff16..9b15f0548ef9 100644 --- a/src/runtime/shell/IOWriter.rs +++ b/src/runtime/shell/IOWriter.rs @@ -13,7 +13,7 @@ //! this simplifies management of the file descriptor. use bun_collections::VecExt; -use core::cell::UnsafeCell; +use bun_jsc::JsCell; #[cfg(not(windows))] use core::ffi::c_void; @@ -202,7 +202,7 @@ impl IOWriter { } } -/// Mutable state. Wrapped in `UnsafeCell` so `Arc`-shared callers can +/// Mutable state. Wrapped in `JsCell` so `Arc`-shared callers can /// mutate via `&self` (single-threaded shell). struct State { writer: WriterImpl, @@ -236,7 +236,7 @@ struct State { } pub struct IOWriter { - state: UnsafeCell, + state: JsCell, } // SAFETY: shell is single-threaded; `Arc` is used purely for refcounting. @@ -247,15 +247,15 @@ unsafe impl Send for IOWriter {} unsafe impl Sync for IOWriter {} impl IOWriter { - /// SAFETY: single-threaded; no overlapping `&mut State` may be live across - /// a re-entrant `enqueue` from a child callback (the `Yield` trampoline - /// runs child callbacks after the borrow is dropped). + /// # Safety + /// Single-threaded only, and no other `&mut State` for this `IOWriter` may + /// be live. Re-derive after anything that can re-enter: `run_yield`, + /// `__start`, `set_writing`, `bump`, `skip_dead`, child callbacks. #[inline] #[allow(clippy::mut_from_ref)] - fn state(&self) -> &mut State { - // SAFETY: single-threaded; callers uphold the no-overlapping-`&mut State` - // invariant documented on this fn (re-derive across re-entrant calls). - unsafe { &mut *self.state.get() } + unsafe fn state(&self) -> &mut State { + // SAFETY: caller upholds the no-overlapping-`&mut State` contract. + unsafe { self.state.get_mut() } } /// Bump our own Arc strong count. Held across re-entrant `run_yield` calls @@ -263,7 +263,8 @@ impl IOWriter { /// mid-method; the stack-held strong ref prevents that. #[inline] fn keepalive(&self) -> std::sync::Arc { - self.state() + self.state + .get() .self_weak .upgrade() .expect("IOWriter::keepalive after last Arc dropped") @@ -273,7 +274,7 @@ impl IOWriter { /// `ShellSubprocess::spawn` to decide `no_sigpipe`). #[inline] pub fn is_socket(&self) -> bool { - self.state().flags.is_socket + self.state.get().flags.is_socket } pub fn init(fd: Fd, flags: Flags, evtloop: EventLoopHandle) -> std::sync::Arc { @@ -288,7 +289,7 @@ impl IOWriter { writer.owns_fd = false; } let this = std::sync::Arc::new_cyclic(|w| IOWriter { - state: UnsafeCell::new(State { + state: JsCell::new(State { writer, fd, writers: Writers::new(), @@ -311,10 +312,11 @@ impl IOWriter { // because the `BufferedWriterParent` callback ABI is `*mut Self`. The // pointer is never used to materialize `&mut IOWriter` — every callback // (`on_write`/`on_error`/`get_buffer`/…) re-enters via `&*this` and - // mutates solely through `UnsafeCell` (`state()`), which carries + // mutates solely through `JsCell` (`state()`), which carries // its own write provenance. No const→mut UB. let parent: *mut IOWriter = std::sync::Arc::as_ptr(&this).cast_mut(); - this.state().writer.set_parent(parent); + // SAFETY: `this` was just constructed; no other `&mut State` is live. + unsafe { this.state() }.writer.set_parent(parent); crate::shell_log!("IOWriter(0x{:x}, fd={}) init", parent as usize, fd); this } @@ -330,22 +332,23 @@ impl IOWriter { #[allow(clippy::not_unsafe_ptr_arg_deref)] #[inline] pub fn set_interp(&self, interp: *mut Interpreter) { - // SAFETY: caller contract above. - self.state().interp = unsafe { bun_ptr::ParentRef::from_nullable_mut(interp) }; + // SAFETY: caller contract above; `from_nullable_mut` does not re-enter. + let parent = unsafe { bun_ptr::ParentRef::from_nullable_mut(interp) }; + self.state.with_mut(|s| s.interp = parent); } #[inline] pub fn fd(&self) -> Fd { - self.state().fd + self.state.get().fd } #[inline] pub fn evtloop(&self) -> EventLoopHandle { - self.state().evtloop + self.state.get().evtloop } pub fn memory_cost(&self) -> usize { - let s = self.state(); + let s = self.state.get(); let mut cost = core::mem::size_of::(); cost += s.buf.capacity(); #[cfg(windows)] @@ -364,15 +367,15 @@ impl IOWriter { #[cfg(not(windows))] #[inline] fn io_evtloop(&self) -> bun_io::EventLoopHandle { - // SAFETY: `bun_io::EventLoopHandle` stores `*mut c_void` purely for - // type-erasure; vtable consumers treat the pointee as read-only - self.state().evtloop.as_event_loop_ctx() + self.state.get().evtloop.as_event_loop_ctx() } // ── start ──────────────────────────────────────────────────────────── fn __start(&self) -> sys::Result<()> { - let s = self.state(); + // SAFETY: no other `&mut State` is live on entry. Dropped before the + // recursive `__start()` calls below, which re-derive. + let s = unsafe { self.state() }; crate::shell_log!("IOWriter(fd={}) __start()", s.fd); if let Err(e) = s.writer.start(s.fd, s.flags.pollable) { #[cfg(not(windows))] @@ -454,10 +457,9 @@ impl IOWriter { #[cfg(not(windows))] { use bun_io::FilePollFlag; - // NOTE: re-derive `state()` — the EINVAL/EPERM fallback paths - // above re-enter `__start()` and mutate `writer.handle`, which - // invalidates `s` under Stacked Borrows. - let s = self.state(); + // SAFETY: the earlier `s` is dead. The EINVAL/EPERM fallbacks above + // re-enter `__start()` and mutate `writer.handle`, so re-derive. + let s = unsafe { self.state() }; if let Some(poll) = s.writer.get_poll() { if s.flags.nonblock { poll.set_flag(FilePollFlag::Nonblocking); @@ -482,7 +484,9 @@ impl IOWriter { /// error completion has to bounce off it (`on_sync_error`) instead of /// re-entering `Yield::run` (see `DbgDepthGuard`). fn write(&self) -> WriteOutcome { - let s = self.state(); + // SAFETY: no other `&mut State` is live on entry; dropped before the + // `__start()` call below, which re-derives. + let s = unsafe { self.state() }; #[cfg(not(windows))] debug_assert!(s.flags.pollable); @@ -496,10 +500,9 @@ impl IOWriter { } #[cfg(not(windows))] { - // NOTE: `__start()` re-derives `state()` (and may mutate - // `writer.handle` on the EINVAL/EPERM fallback paths), which - // invalidates the `s` borrow under Stacked Borrows. Re-derive. - let s = self.state(); + // SAFETY: the outer `s` is dead. `__start()` re-derived and may + // have mutated `writer.handle`, so re-derive here. + let s = unsafe { self.state() }; // if `handle == .fd` it means it's a file which does not // support polling for writeability and we should just write to it if matches!(s.writer.handle, bun_io::pipes::PollOrFd::Fd(_)) { @@ -549,7 +552,8 @@ impl IOWriter { /// Cancel the chunks enqueued by the given child by marking them as dead. pub fn cancel_chunks(&self, ptr: ChildPtr) { - let s = self.state(); + // SAFETY: no other `&mut State` is live; nothing below re-enters. + let s = unsafe { self.state() }; if s.writers.is_empty() { return; } @@ -567,7 +571,8 @@ impl IOWriter { /// Skips over dead children and increments `total_bytes_written` by the /// amount they would have written so the buf is skipped as well. fn skip_dead(&self) { - let s = self.state(); + // SAFETY: no other `&mut State` is live; nothing below re-enters. + let s = unsafe { self.state() }; while s.writer_idx < s.writers.len() { let w = &s.writers[s.writer_idx]; if w.is_dead() { @@ -580,7 +585,7 @@ impl IOWriter { } fn wrote_everything(&self) -> bool { - let s = self.state(); + let s = self.state.get(); s.total_bytes_written >= s.buf.len() } @@ -589,7 +594,7 @@ impl IOWriter { fn set_writing(&self, writing: bool) { #[cfg(windows)] { - self.state().is_writing = writing; + self.state.with_mut(|s| s.is_writing = writing); } let _ = writing; } @@ -602,7 +607,9 @@ impl IOWriter { let result = self.get_buffer_impl(); #[cfg(windows)] { - let s = self.state(); + // SAFETY: `get_buffer_impl`'s `&mut State` is dead; `result` points + // into `buf`'s heap allocation, disjoint from `State`. + let s = unsafe { self.state() }; s.winbuf.clear(); s.winbuf.extend_from_slice(result); // `state()` ties `s` to `&self`, so the slice borrow already has @@ -614,10 +621,11 @@ impl IOWriter { } fn get_buffer_impl(&self) -> &[u8] { - // NOTE: reshaped for borrowck — re-derive `state()` after - // `skip_dead()` instead of holding one `&mut State` across it. + // Scoped so the first `&mut State` dies before `skip_dead()` re-derives. { - let s = self.state(); + // SAFETY: no other `&mut State` is live; `s` is dropped before + // `skip_dead()` takes its own. + let s = unsafe { self.state() }; if s.writer_idx >= s.writers.len() { return &[]; } @@ -626,7 +634,8 @@ impl IOWriter { self.skip_dead(); } } - let s = self.state(); + // SAFETY: the scoped `s` above and `skip_dead()`'s borrow are both dead. + let s = unsafe { self.state() }; if s.writer_idx >= s.writers.len() { return &[]; } @@ -647,11 +656,10 @@ impl IOWriter { /// Advance past `current_writer`, shrinking `buf` if appropriate, and /// return the `Yield` for the child's `on_io_writer_chunk` callback. fn bump(&self, current_idx: usize) -> Yield { - // NOTE: reshaped for borrowck — `skip_dead()` re-derives `state()`, - // so we must drop `s` before calling it and re-derive after, otherwise - // two `&mut State` are live simultaneously (UB under Stacked Borrows). + // Scoped so each `&mut State` dies before `skip_dead()` re-derives. let (is_dead, written, child_ptr) = { - let s = self.state(); + // SAFETY: no other `&mut State` is live; `s` dies with this scope. + let s = unsafe { self.state() }; let w = &s.writers[current_idx]; (w.is_dead(), w.written, w.ptr) }; @@ -659,12 +667,14 @@ impl IOWriter { if is_dead { self.skip_dead(); } else { - let s = self.state(); + // SAFETY: the scoped `s` above is dead; nothing here re-enters. + let s = unsafe { self.state() }; debug_assert!(s.writers[current_idx].written == s.writers[current_idx].len); s.writer_idx += 1; } - let s = self.state(); + // SAFETY: the borrows above (and `skip_dead()`'s) are all dead. + let s = unsafe { self.state() }; if s.writer_idx >= s.writers.len() { s.buf.clear(); s.writer_idx = 0; @@ -697,7 +707,7 @@ impl IOWriter { #[cfg(not(windows))] fn do_file_write(&self, child: ChildPtr) -> Yield { { - let s = self.state(); + let s = self.state.get(); debug_assert!(!s.flags.pollable); debug_assert!(s.writer_idx < s.writers.len()); } @@ -705,8 +715,9 @@ impl IOWriter { scopeguard::defer! { self.set_writing(false); } self.skip_dead(); - let idx = self.state().writer_idx; - debug_assert!(!self.state().writers[idx].is_dead()); + // `skip_dead()`'s borrow is already dead. + let idx = self.state.get().writer_idx; + debug_assert!(!self.state.get().writers[idx].is_dead()); let buf = self.get_buffer(); debug_assert!(!buf.is_empty()); @@ -725,7 +736,9 @@ impl IOWriter { // error completion is returned, not `Yield::run` from here. bun_io::WriteResult::Err(e) => return self.on_sync_error(child, &e), }; - let s = self.state(); + // SAFETY: `get_buffer()`'s and `drain_buffered_data()`'s borrows are + // dead; nothing below re-enters before `bump()` re-derives. + let s = unsafe { self.state() }; let lo = s.total_bytes_written; s.writers[idx].tee(&s.buf[lo..lo + amt]); s.total_bytes_written += amt; @@ -745,11 +758,12 @@ impl IOWriter { /// The `BufferedWriter.onWrite` hook. Runs on the event loop when the fd /// is writable. fn on_write_pollable(&self, amount: usize, status: bun_io::WriteStatus) { - // NOTE: `set_writing` re-derives `state()` on Windows, which would - // invalidate `s` under Stacked Borrows; do it before binding `s` - // (matches the ordering in `on_error`). + // NOTE: `set_writing` re-derives `state()` on Windows, so it must run + // before `s` is bound (matches the ordering in `on_error`). self.set_writing(false); - let s = self.state(); + // SAFETY: `set_writing`'s borrow is dead. `s` is dropped before every + // `bump()`/`run_yield()` below, each of which re-derives. + let s = unsafe { self.state() }; #[cfg(not(windows))] debug_assert!(s.flags.pollable); @@ -790,7 +804,8 @@ impl IOWriter { } let wrote_everything = self.wrote_everything(); - let s = self.state(); + // SAFETY: every borrow above (incl. `run_yield`'s JS re-entry) is dead. + let s = unsafe { self.state() }; if !wrote_everything && s.writer_idx < s.writers.len() { #[cfg(windows)] { @@ -810,7 +825,9 @@ impl IOWriter { } fn broken_pipe_for_writers(&self) { - let s = self.state(); + // SAFETY: no other `&mut State` is live; `s` is dropped before the + // `run_yield()`/`cancel_chunks()` loop below, which can re-enter. + let s = unsafe { self.state() }; debug_assert!(s.flags.broken_pipe); // NOTE: reshaped for borrowck — collect targets first so we don't // hold `&mut s.writers` across `cancel_chunks`/`run_yield`. @@ -832,7 +849,9 @@ impl IOWriter { }); self.cancel_chunks(ptr); } - let s = self.state(); + // SAFETY: the `run_yield()` JS re-entry and `cancel_chunks()` borrows + // are dead; re-derive after the loop. + let s = unsafe { self.state() }; s.total_bytes_written = 0; s.writers.clear(); s.buf.clear(); @@ -845,7 +864,9 @@ impl IOWriter { /// re-enqueueing from its callback is not wiped afterwards. fn fail_pending_writers(&self, err: &sys::Error) -> Vec { self.set_writing(false); - let s = self.state(); + // SAFETY: `set_writing`'s borrow is dead; nothing below re-enters (the + // completions are returned to the caller, not run here). + let s = unsafe { self.state() }; if err.get_errno() == E::EPIPE { s.flags.broken_pipe = true; } @@ -930,7 +951,8 @@ impl IOWriter { /// Drive a `Yield` from inside an async poll callback. Requires `interp` /// to have been set; if not, the chunk-complete is dropped (debug-asserts). fn run_yield(&self, y: Yield) { - let Some(interp) = self.state().interp else { + // Read out here; the borrow dies before `y.run()` re-enters. + let Some(interp) = self.state.get().interp else { debug_assert!( matches!(y, Yield::Done), "IOWriter async callback fired without interp backref" @@ -955,7 +977,9 @@ impl IOWriter { /// flavor of the same thing. Report the error to the child instead of /// queueing the chunk. fn handle_dead_writer(&self, ptr: ChildPtr) -> Option { - let s = self.state(); + // SAFETY: callers drop any live `&mut State` first; nothing here + // re-enters (the `Yield` is returned, not run). + let s = unsafe { self.state() }; if s.flags.broken_pipe { let err = sys::Error::from_code(E::EPIPE, sys::Tag::write).to_system_error(); return Some(Yield::OnIoWriterChunk { @@ -978,7 +1002,9 @@ impl IOWriter { #[cfg(not(windows))] fn enqueue_file(&self, child: ChildPtr) -> Yield { - let s = self.state(); + // SAFETY: no other `&mut State` is live; `s` is dropped before + // `set_writing()`/`do_file_write()`, which re-derive. + let s = unsafe { self.state() }; if s.is_writing { return Yield::suspended(); } @@ -992,10 +1018,11 @@ impl IOWriter { /// You MUST have already added the data to `self.buf`! /// `child` is the writer that was just pushed (see `on_sync_error`). fn enqueue_internal(&self, child: ChildPtr) -> Yield { - debug_assert!(!self.state().flags.broken_pipe); - debug_assert!(self.state().err.is_none()); + // `enqueue`'s borrow is already dead. + debug_assert!(!self.state.get().flags.broken_pipe); + debug_assert!(self.state.get().err.is_none()); #[cfg(not(windows))] - if !self.state().flags.pollable { + if !self.state.get().flags.pollable { return self.enqueue_file(child); } match self.write() { @@ -1019,7 +1046,9 @@ impl IOWriter { err: None, }; } - let s = self.state(); + // SAFETY: `handle_dead_writer`'s borrow is dead; `s` is dropped before + // `enqueue_internal()`, which re-derives. + let s = unsafe { self.state() }; s.buf.extend_from_slice(buf); s.writers.push(Writer { ptr: child, @@ -1039,7 +1068,9 @@ impl IOWriter { args: core::fmt::Arguments<'_>, ) -> Yield { use std::io::Write as _; - let s = self.state(); + // SAFETY: no other `&mut State` is live; `s` is dropped before + // `enqueue_internal()`, which re-derives. + let s = unsafe { self.state() }; let start = s.buf.len(); if let Some(k) = kind { let _ = write!(&mut s.buf, "{}: ", k.as_str()); @@ -1164,7 +1195,8 @@ fn drain_buffered_data( }; let mut drained: usize = 0; while drained < trimmed.len() { - match try_write_with_write_fn(parent.state().fd, buf, sys::write) { + let fd = parent.state.get().fd; + match try_write_with_write_fn(fd, buf, sys::write) { bun_io::WriteResult::Pending(pending) => { drained += pending; return bun_io::WriteResult::Pending(drained); @@ -1199,7 +1231,8 @@ impl Drop for IOWriter { // Arc drops (possible via re-entrant child deinit), we need the async // hop. Revisit once `bun_event_loop::EventLoopTask` is wired to the // shell's `EventLoopHandle` shim. - let s = self.state.get_mut(); + // SAFETY: `&mut self` in `drop` — unique access, no other borrow. + let s = unsafe { self.state.get_mut() }; crate::shell_log!("IOWriter(fd={}) deinit", s.fd); #[cfg(not(windows))] { diff --git a/src/runtime/shell/builtin/cp.rs b/src/runtime/shell/builtin/cp.rs index 854e2bb415fb..7015000160ae 100644 --- a/src/runtime/shell/builtin/cp.rs +++ b/src/runtime/shell/builtin/cp.rs @@ -52,8 +52,11 @@ pub struct ExecState { /// ignores the EBUSY if at least one task succeeded for that dest. #[derive(Default)] pub struct EbusyState { - pub tasks: Vec<*mut ShellCpTask>, - pub idx: usize, + /// `vec_box` is wrong here: each task's address is handed to the work pool + /// (`WorkPool::schedule(&raw mut (*st).task)`), so the elements must be + /// heap-stable. A `Vec` would move them on realloc. + #[allow(clippy::vec_box)] + pub tasks: Vec>, pub main_exit_code: ExitCode, /// Absolute target paths that some task copied successfully — used to /// suppress a sibling task's EBUSY on the same target. @@ -161,7 +164,6 @@ impl Cp { unreachable!() }; let mut ebusy = core::mem::take(&mut exec.ebusy); - ebusy.idx = 0; ebusy.main_exit_code = exit_code; Self::state_mut(interp, cmd).state = State::Ebusy(ebusy); Self::ignore_ebusy_error_if_possible(interp, cmd) @@ -217,36 +219,28 @@ impl Cp { #[cfg(windows)] fn ignore_ebusy_error_if_possible(interp: &Interpreter, cmd: NodeId) -> Yield { loop { - // Pop tasks one at a time; `idx` is bumped on the first - // non-ignorable hit so a re-entry resumes there. + // Take tasks one at a time off the front, so a re-entry (driven by + // `print_shell_cp_task`) resumes at the next one. let next = { let State::Ebusy(eb) = &mut Self::state_mut(interp, cmd).state else { unreachable!() }; - if eb.idx < eb.tasks.len() { - let t = eb.tasks[eb.idx]; - eb.idx += 1; - // SAFETY: `t` is a live heap-allocated task stashed in - // `on_shell_cp_task_done`; not yet freed. - let tref = unsafe { &*t }; - let ignorable = tref + if eb.tasks.is_empty() { + None + } else { + let t = eb.tasks.remove(0); + let ignorable = t .tgt_absolute .as_ref() .map_or(false, |p| eb.absolute_targets.contains(p)) - || tref - .src_absolute + || t.src_absolute .as_ref() .map_or(false, |p| eb.absolute_srcs.contains(p)); Some((t, ignorable)) - } else { - None } }; match next { - Some((t, true)) => { - // SAFETY: paired with `heap::alloc` in `create()`. - drop(unsafe { bun_core::heap::take(t) }); - } + Some((t, true)) => drop(t), Some((t, false)) => return Self::print_shell_cp_task(interp, cmd, t), None => break, } @@ -260,27 +254,28 @@ impl Cp { Builtin::done(interp, cmd, exit_code) } - pub(crate) fn on_shell_cp_task_done(interp: &Interpreter, cmd: NodeId, task: *mut ShellCpTask) { + pub(crate) fn on_shell_cp_task_done(interp: &Interpreter, cmd: NodeId, task: Box) { if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { exec.tasks_count -= 1; } #[cfg(windows)] - { - // SAFETY: `task` is a live heap-allocated task; main-thread only. - let tref = unsafe { &mut *task }; + let task = { + let mut task = task; + // Defer the task to the ebusy phase. Note the precedence: + // `(is_sys && errno==EBUSY && tgt_match) || src_match` + // i.e. ANY sys error whose `path` equals `src_absolute` is + // deferred regardless of errno; preserved deliberately for + // compatibility. + let is_ebusy = task.err.as_ref().map_or(false, |err| { + matches!(err, ShellErr::Sys(sys) + if (sys.get_errno() == bun_sys::E::EBUSY + && task.tgt_absolute.as_deref() + .map_or(false, |p| sys.path.eql_utf8(p))) + || task.src_absolute.as_deref() + .map_or(false, |p| sys.path.eql_utf8(p))) + }); if let State::Exec(exec) = &mut Self::state_mut(interp, cmd).state { - if let Some(err) = &tref.err { - // Defer the task to the ebusy phase. Note the precedence: - // `(is_sys && errno==EBUSY && tgt_match) || src_match` - // i.e. ANY sys error whose `path` equals `src_absolute` is - // deferred regardless of errno; preserved deliberately for - // compatibility. - let is_ebusy = matches!(err, ShellErr::Sys(sys) - if (sys.get_errno() == bun_sys::E::EBUSY - && tref.tgt_absolute.as_deref() - .map_or(false, |p| sys.path.eql_utf8(p))) - || tref.src_absolute.as_deref() - .map_or(false, |p| sys.path.eql_utf8(p))); + if task.err.is_some() { if is_ebusy { exec.ebusy.tasks.push(task); return Self::next(interp, cmd).run(interp); @@ -288,21 +283,22 @@ impl Cp { } else { // Record successful absolute paths so a deferred EBUSY // sibling can be suppressed. - if let Some(tgt) = tref.tgt_absolute.take() { + if let Some(tgt) = task.tgt_absolute.take() { bun_core::handle_oom(exec.ebusy.absolute_targets.insert(&tgt)); } - if let Some(src) = tref.src_absolute.take() { + if let Some(src) = task.src_absolute.take() { bun_core::handle_oom(exec.ebusy.absolute_srcs.insert(&src)); } } } - } + task + }; Self::print_shell_cp_task(interp, cmd, task).run(interp); } - fn print_shell_cp_task(interp: &Interpreter, cmd: NodeId, task: *mut ShellCpTask) -> Yield { - // SAFETY: task was heap-allocated in create(); reclaim. - let mut task = unsafe { bun_core::heap::take(task) }; + // `boxed_local`: the `Box` is the ownership unit being reclaimed here. + #[allow(clippy::boxed_local)] + fn print_shell_cp_task(interp: &Interpreter, cmd: NodeId, mut task: Box) -> Yield { // The lock is uncontended here (all work-pool subtasks have // finished) but the data lives inside it. let output = core::mem::take(&mut *task.verbose_output.lock()); @@ -741,8 +737,8 @@ impl ShellCpTask { /// ownership is consumed via [`Cp::on_shell_cp_task_done`]. pub(crate) fn run_from_main_thread(this: *mut ShellCpTask, interp: &Interpreter) { // SAFETY: `this` is a live heap-allocated task per the caller's contract. - let cmd = unsafe { (*this).cmd }; - Cp::on_shell_cp_task_done(interp, cmd, this); + let this = unsafe { bun_core::heap::take(this) }; + Cp::on_shell_cp_task_done(interp, this.cmd, this); } } diff --git a/src/runtime/shell/dispatch_tasks.rs b/src/runtime/shell/dispatch_tasks.rs index ad65e64d9f0b..3057e22ec811 100644 --- a/src/runtime/shell/dispatch_tasks.rs +++ b/src/runtime/shell/dispatch_tasks.rs @@ -35,24 +35,12 @@ impl ShellAsyncSubprocessDone { /// Reached only via `runtime::dispatch::run_task` for /// `task_tag::ShellAsyncSubprocessDone`, which always passes the /// `heap::alloc` payload enqueued by `ShellSubprocess::on_process_exit`. - /// - /// # Safety - /// `this` must be the live `heap::alloc` payload enqueued by - /// `ShellSubprocess::on_process_exit`, and `(*this).interp` must outlive - /// the call. Ownership of `*this` is consumed. - // Dispatch trampoline: `this` validity is guaranteed by the `run_task` - // contract; signature is fixed by `dispatch.rs`. - #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub(crate) fn run_from_main_thread(this: *mut Self) { - // SAFETY: dispatch contract — `this` is the live `heap::alloc` payload - // enqueued by `ShellSubprocess::on_process_exit`; `interp` outlives - // every spawned subprocess. - let (owned, interp) = unsafe { - let owned = bun_core::heap::take(this); - let interp = &*owned.interp; - (owned, interp) - }; - crate::shell::states::cmd::Cmd::on_subprocess_done(interp, owned.cmd, owned.exit_code); + // `boxed_local`: the `Box` is the ownership unit being reclaimed here. + #[allow(clippy::boxed_local)] + pub(crate) fn run_from_main_thread(self: Box) { + // SAFETY: `interp` outlives every spawned subprocess. + let interp = unsafe { &*self.interp }; + crate::shell::states::cmd::Cmd::on_subprocess_done(interp, self.cmd, self.exit_code); } } @@ -67,20 +55,12 @@ pub(crate) struct AsyncDeinitWriter { impl AsyncDeinitWriter { /// Reached only via `runtime::dispatch::run_task` for - /// `task_tag::ShellIOWriterAsyncDeinit`, which always passes the - /// `heap::alloc` payload enqueued by `IOWriter::async_deinit`. - /// - /// # Safety - /// `this` must be the live `heap::alloc` payload enqueued by - /// `IOWriter::async_deinit`. Ownership of `*this` is consumed. - // Dispatch trampoline: `this` validity is guaranteed by the `run_task` - // contract; signature is fixed by `dispatch.rs`. - #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub(crate) fn run_from_main_thread(this: *mut Self) { - // SAFETY: dispatch contract — `this` is the live `heap::alloc` payload - // enqueued by `IOWriter::async_deinit`. - let owned = unsafe { bun_core::heap::take(this) }; - crate::shell::io_writer::IOWriter::deinit_on_main_thread(owned.writer); + /// `task_tag::ShellIOWriterAsyncDeinit`, which hands over the `heap::alloc` + /// payload enqueued by `IOWriter::async_deinit`. + // `boxed_local`: the `Box` is the ownership unit being reclaimed here. + #[allow(clippy::boxed_local)] + pub(crate) fn run_from_main_thread(self: Box) { + crate::shell::io_writer::IOWriter::deinit_on_main_thread(self.writer); } } @@ -96,18 +76,10 @@ impl AsyncDeinitReader { /// Reached only via `runtime::dispatch::run_task` for /// `task_tag::ShellIOReaderAsyncDeinit`, which always passes the /// `heap::alloc` payload enqueued by `IOReader::async_deinit`. - /// - /// # Safety - /// `this` must be the live `heap::alloc` payload enqueued by - /// `IOReader::async_deinit`. Ownership of `*this` is consumed. - // Dispatch trampoline: `this` validity is guaranteed by the `run_task` - // contract; signature is fixed by `dispatch.rs`. - #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub(crate) fn run_from_main_thread(this: *mut Self) { - // SAFETY: dispatch contract — `this` is the live `heap::alloc` payload - // enqueued by `IOReader::async_deinit`. - let owned = unsafe { bun_core::heap::take(this) }; - crate::shell::io_reader::IOReader::deinit_on_main_thread(owned.reader); + // `boxed_local`: the `Box` is the ownership unit being reclaimed here. + #[allow(clippy::boxed_local)] + pub(crate) fn run_from_main_thread(self: Box) { + crate::shell::io_reader::IOReader::deinit_on_main_thread(self.reader); } } diff --git a/src/runtime/shell/interpreter.rs b/src/runtime/shell/interpreter.rs index 693b33fa798c..5cc70f61bfd3 100644 --- a/src/runtime/shell/interpreter.rs +++ b/src/runtime/shell/interpreter.rs @@ -28,6 +28,7 @@ use bun_collections::VecExt; use bun_core::WTFStringImplExt as _; use bun_jsc::JsCell; +use bun_ptr::BackRef; use core::cell::Cell; use core::fmt; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; @@ -505,15 +506,14 @@ impl Interpreter { let export_env = if matches!(event_loop, EventLoopHandle::Js { .. }) { export_env_.unwrap_or_else(EnvMap::init) } else { - // SAFETY: `event_loop.env()` returns the `MiniEventLoop`'s - // `DotEnv::Loader`, which is set by `init_global()` and outlives - // the interpreter (thread-lifetime singleton). - let env_loader = unsafe { &mut *event_loop.env() }; + // `event_loop.env()` back-references the `MiniEventLoop`'s + // `DotEnv::Loader`, set by `init_global()` and outliving the + // interpreter (thread-lifetime singleton). + let env_loader = event_loop.env(); let mut export_env = EnvMap::init_with_capacity(env_loader.map.map.count()); - let mut iter = env_loader.iterator(); - while let Some(entry) = iter.next() { - let key = crate::shell::EnvStr::init_slice(&entry.key_ptr[..]); - let value = crate::shell::EnvStr::init_slice(&entry.value_ptr.value[..]); + for (key, value) in env_loader.map.iter() { + let key = crate::shell::EnvStr::init_slice(&key[..]); + let value = crate::shell::EnvStr::init_slice(&value.value[..]); export_env.insert(key, value); } export_env @@ -1223,8 +1223,11 @@ impl Interpreter { // `Bun.$` API can read stdout/stderr after completion. The mini path // does not capture (it writes straight to the dup'd fd). let (cap_out, cap_err) = if matches!(event_loop, EventLoopHandle::Js { .. }) { - self.root_shell - .with_mut(|rs| (Some(rs.buffered_stdout()), Some(rs.buffered_stderr()))) + let rs = self.root_shell.get(); + ( + Some(rs.buffered_stdout().as_ptr()), + Some(rs.buffered_stderr().as_ptr()), + ) } else { (None, None) }; @@ -1372,14 +1375,13 @@ impl Interpreter { if free_buffered_io { // Can safely be called multiple times. - self.root_shell.with_mut(|rs| { - if let Bufio::Owned(o) = &mut rs._buffered_stderr { - o.clear_and_free(); - } - if let Bufio::Owned(o) = &mut rs._buffered_stdout { - o.clear_and_free(); - } - }); + let rs = self.root_shell.get(); + if let Bufio::Owned(o) = &rs._buffered_stderr { + o.with_mut(|o| o.clear_and_free()); + } + if let Bufio::Owned(o) = &rs._buffered_stdout { + o.with_mut(|o| o.clear_and_free()); + } } // Has this already been finalized? @@ -1419,13 +1421,9 @@ impl Interpreter { /// GC finalizer body — runs /// whatever teardown `finish()` didn't, then frees the box. - /// - /// # Safety - /// `this` must be the `heap::alloc`'d pointer stored in the JS wrapper's - /// `m_ctx`; called exactly once from the GC thread's finalizer. - pub unsafe fn deinit_from_finalizer(this: *mut Self) { - // SAFETY: caller contract — `this` is a live `heap::alloc` payload. - let this = unsafe { bun_core::heap::take(this) }; + // `boxed_local`: the `Box` is the ownership unit being reclaimed here. + #[allow(clippy::boxed_local)] + pub fn deinit_from_finalizer(this: Box) { log!( "Interpreter(0x{:x}) deinitFromFinalizer (cleanup_state={})", &raw const *this as usize, @@ -1474,6 +1472,8 @@ impl Interpreter { // `args: Box` and `vm_args_utf8: Vec` drop // with the box; `ZigStringSlice` has a `Drop` impl that derefs its // WTF backing. + // The `Box` is the ownership unit the finalizer handed back; release it. + drop(this); } /// JS `interp.setQuiet()`. @@ -1580,20 +1580,14 @@ impl Interpreter { &self, global_this: &crate::jsc::JSGlobalObject, ) -> crate::jsc::JSValue { - io_to_js_value( - global_this, - self.root_shell.with_mut(|rs| rs.buffered_stdout()), - ) + io_to_js_value(global_this, self.root_shell.get().buffered_stdout()) } pub fn get_buffered_stderr( &self, global_this: &crate::jsc::JSGlobalObject, ) -> crate::jsc::JSValue { - io_to_js_value( - global_this, - self.root_shell.with_mut(|rs| rs.buffered_stderr()), - ) + io_to_js_value(global_this, self.root_shell.get().buffered_stderr()) } /// GC finalizer hook — called from the @@ -1601,10 +1595,7 @@ impl Interpreter { /// `host_fn::host_fn_finalize`. pub fn finalize(self: Box) { log!("Interpreter(0x{:x}) finalize", &raw const *self as usize); - // See [`deinit_from_finalizer`](Self::deinit_from_finalizer). - // SAFETY: `self` is the unique GC-owned `m_ctx` payload; round-trip via - // raw ptr so `deinit_from_finalizer` can `heap::take` it. - unsafe { Self::deinit_from_finalizer(Box::into_raw(self)) }; + Self::deinit_from_finalizer(self); } /// GC `hasPendingActivity()` hook. @@ -1740,10 +1731,11 @@ impl Interpreter { /// and resets the source to empty. fn io_to_js_value( global_this: &crate::jsc::JSGlobalObject, - buf: *mut Vec, + buf: &JsCell>, ) -> crate::jsc::JSValue { - // SAFETY: `buf` points into a live `ShellExecEnv` (root or borrowed). - let bytelist = core::mem::take(unsafe { &mut *buf }); + // Take the bytes out before touching JS: no borrow of `buf` is live across + // `create_buffer`, which can re-enter. + let bytelist = buf.replace(Vec::new()); // The moved-out `Vec` storage is handed to JSC directly; the // `Vec` value itself is `mem::forget`-ed since JSC now owns the bytes. let mut bytelist = core::mem::ManuallyDrop::new(bytelist); @@ -1796,23 +1788,21 @@ pub struct ShellExecEnv { } pub enum Bufio { - Owned(Vec), - Borrowed(*mut Vec), + Owned(JsCell>), + Borrowed(BackRef>>), } impl Default for Bufio { fn default() -> Self { - Bufio::Owned(Vec::::default()) + Bufio::Owned(JsCell::new(Vec::::default())) } } impl Bufio { pub fn memory_cost(&self) -> usize { match self { - Bufio::Owned(o) => o.memory_cost(), - // SAFETY: borrowed buffer points into a live parent `ShellExecEnv` - // (set by `dupe_for_subshell`); the parent outlives the child. - Bufio::Borrowed(b) => unsafe { (**b).memory_cost() }, + Bufio::Owned(o) => o.get().memory_cost(), + Bufio::Borrowed(b) => b.get().get().memory_cost(), } } } @@ -1857,54 +1847,23 @@ impl ShellExecEnv { &self.__prev_cwd[..self.__prev_cwd.len().saturating_sub(1)] } - pub fn buffered_stdout(&mut self) -> *mut Vec { - // Return the raw `*mut` directly — no `&mut Vec` is materialised, - // so the `Bufio::Borrowed` aliasing concern (which forces - // [`buffered_stdout_mut`] to be `unsafe fn`) does not apply here. The - // dereference obligation is on whoever later writes through it. - match &mut self._buffered_stdout { - Bufio::Owned(o) => std::ptr::from_mut(o), - Bufio::Borrowed(b) => *b, - } - } - - pub fn buffered_stderr(&mut self) -> *mut Vec { - match &mut self._buffered_stderr { - Bufio::Owned(o) => std::ptr::from_mut(o), - Bufio::Borrowed(b) => *b, - } - } - - /// Mutably borrow the captured-stdout buffer (owned, or the parent env's - /// buffer for subshell/pipeline children — see `Bufio`). - /// - /// # Safety - /// In the `Bufio::Borrowed` arm the returned `&mut Vec` aliases the - /// PARENT `ShellExecEnv`'s buffer. Caller must ensure no other - /// `&`/`&mut` to that buffer is live (including via a `&mut` of the - /// parent env). The shell trampoline mutates one node at a time so this - /// holds in practice, but `&mut self` alone does not encode it — hence - /// `unsafe fn`. The parent env strictly outlives this child (parents - /// `deinit` after children), so the pointer is never dangling. + /// The captured-stdout cell: this env's own buffer, or the parent env's + /// for subshell/pipeline children (see `Bufio`). Writers go through + /// `JsCell::with_mut`, so no `&mut Vec` ever aliases the parent. #[inline] - pub unsafe fn buffered_stdout_mut(&mut self) -> &mut Vec { - match &mut self._buffered_stdout { + pub fn buffered_stdout(&self) -> &JsCell> { + match &self._buffered_stdout { Bufio::Owned(o) => o, - // SAFETY: caller contract. - Bufio::Borrowed(b) => unsafe { &mut **b }, + Bufio::Borrowed(b) => b.get(), } } - /// See [`buffered_stdout_mut`]. - /// - /// # Safety - /// See [`buffered_stdout_mut`]. + /// See [`buffered_stdout`]. #[inline] - pub unsafe fn buffered_stderr_mut(&mut self) -> &mut Vec { - match &mut self._buffered_stderr { + pub fn buffered_stderr(&self) -> &JsCell> { + match &self._buffered_stderr { Bufio::Owned(o) => o, - // SAFETY: caller contract; see `buffered_stdout_mut`. - Bufio::Borrowed(b) => unsafe { &mut **b }, + Bufio::Borrowed(b) => b.get(), } } @@ -1927,16 +1886,18 @@ impl ShellExecEnv { // For `.fd` with a captured // buffer, borrow that; for `.ignore`, own a fresh one; for `.pipe`, // own when normal/cmd_subst, borrow parent's when subshell/pipeline. - let bufio_for = |out: &OutKind, parent_buf: *mut Vec| -> Bufio { + let bufio_for = |out: &OutKind, parent_buf: BackRef>>| -> Bufio { match out { OutKind::Fd(f) => match f.captured { - Some(cap) => Bufio::Borrowed(cap), - None => Bufio::Owned(Vec::::default()), + // SAFETY: `captured` is `JsCell::as_ptr` of a live parent + // env's buffer cell; the parent outlives this child. + Some(cap) => Bufio::Borrowed(unsafe { BackRef::from_raw(cap.cast()) }), + None => Bufio::Owned(JsCell::new(Vec::::default())), }, - OutKind::Ignore => Bufio::Owned(Vec::::default()), + OutKind::Ignore => Bufio::Owned(JsCell::new(Vec::::default())), OutKind::Pipe => match kind { ShellExecEnvKind::Normal | ShellExecEnvKind::CmdSubst => { - Bufio::Owned(Vec::::default()) + Bufio::Owned(JsCell::new(Vec::::default())) } ShellExecEnvKind::Subshell | ShellExecEnvKind::Pipeline => { Bufio::Borrowed(parent_buf) @@ -1944,8 +1905,8 @@ impl ShellExecEnv { }, } }; - let stdout = bufio_for(&io.stdout, self.buffered_stdout()); - let stderr = bufio_for(&io.stderr, self.buffered_stderr()); + let stdout = bufio_for(&io.stdout, BackRef::new(self.buffered_stdout())); + let stderr = bufio_for(&io.stderr, BackRef::new(self.buffered_stderr())); let duped = Box::new(ShellExecEnv { kind, @@ -1977,7 +1938,7 @@ impl ShellExecEnv { let boxed = unsafe { bun_core::heap::take(this) }; closefd(boxed.cwd_fd); // EnvMap/Vec/Vec drop impls free their storage; `Bufio::Borrowed` - // is a raw ptr so its drop is a no-op. + // is a non-owning `BackRef` so its drop is a no-op. drop(boxed); } @@ -1989,11 +1950,11 @@ impl ShellExecEnv { std::ptr::from_ref(self) as usize ); if free_buffered_io { - if let Bufio::Owned(o) = &mut self._buffered_stdout { - o.clear_and_free(); + if let Bufio::Owned(o) = &self._buffered_stdout { + o.with_mut(|o| o.clear_and_free()); } - if let Bufio::Owned(o) = &mut self._buffered_stderr { - o.clear_and_free(); + if let Bufio::Owned(o) = &self._buffered_stderr { + o.with_mut(|o| o.clear_and_free()); } } // EnvMap has a Drop impl; replace with fresh to free now and leave diff --git a/src/runtime/shell/shell_body.rs b/src/runtime/shell/shell_body.rs index 6b2e39aae883..0e7084e826c4 100644 --- a/src/runtime/shell/shell_body.rs +++ b/src/runtime/shell/shell_body.rs @@ -279,16 +279,12 @@ impl<'a> GlobalJS<'a> { #[inline] pub fn enqueue_task_concurrent_wait_pid(self, task: *mut T) { - // SAFETY: bun_vm_concurrently() returns a valid &VirtualMachine; we need &mut for the - // intrusive concurrent queue push (which is itself thread-safe). The VM outlives the call. - let vm = self - .global_this - .bun_vm_concurrently() - .cast_const() - .cast_mut(); + // SAFETY: `bun_vm_concurrently()` yields the VM owning this global; it is + // process-lifetime and outlives every task queued against it. + let vm: bun_ptr::ParentRef = + unsafe { bun_ptr::ParentRef::from_raw(self.global_this.bun_vm_concurrently()) }; let concurrent = bun_event_loop::ConcurrentTask::create(bun_event_loop::Task::init(task)); - // SAFETY: see above — `vm` is a live VM pointer. - unsafe { &mut *vm }.enqueue_task_concurrent(concurrent); + vm.enqueue_task_concurrent(concurrent); } #[inline] @@ -313,7 +309,9 @@ impl<'a> GlobalJS<'a> { #[cfg(not(windows))] // SAFETY: `event_loop_handle` is set during VM init and never freed before the VM. unsafe { - &*vm.event_loop_handle.expect("event_loop_handle is null") + &*vm.event_loop_handle + .expect("event_loop_handle is null") + .as_ptr() } } diff --git a/src/runtime/shell/states/Cmd.rs b/src/runtime/shell/states/Cmd.rs index 620ed16d2d17..b2e58f0e86a7 100644 --- a/src/runtime/shell/states/Cmd.rs +++ b/src/runtime/shell/states/Cmd.rs @@ -66,7 +66,7 @@ impl Cmd { } pub struct SubprocExec { - pub child: *mut ShellSubprocess, + pub child: Option>, pub buffered_closed: BufferedIoClosed, /// NodeId-arena backrefs so the legacy `&mut self` subprocess callbacks /// (`buffered_output_close` / `on_exit`) can hand a [`Yield`] back to the @@ -585,7 +585,7 @@ impl Cmd { let interp_ptr: *mut Interpreter = interp.as_ctx_ptr(); let buffered_closed = BufferedIoClosed::from_stdio(&spawn_args.stdio); interp.as_cmd_mut(this).exec = Exec::Subproc(Box::new(SubprocExec { - child: core::ptr::null_mut(), + child: None, buffered_closed, interp: core::ptr::null_mut(), this_id: this, @@ -611,7 +611,10 @@ impl Cmd { } spawn_args.argv.push(core::ptr::null()); match &mut cmd.exec { - Exec::Subproc(sub) => core::ptr::addr_of_mut!(sub.child), + // `Option>` shares `*mut T`'s layout, with `None` == null. + Exec::Subproc(sub) => { + core::ptr::addr_of_mut!(sub.child).cast::<*mut ShellSubprocess>() + } _ => unreachable!(), } }; @@ -640,27 +643,31 @@ impl Cmd { if let Err(e) = spawn_result { drop(arena); - // Revert exec so `deinit` doesn't free a null `child`. + // `spawn_async` freed the subprocess and cleared `child` on failure, + // so dropping the exec here frees nothing. interp.as_cmd_mut(this).exec = Exec::None; return Builtin::cmd_write_failing_error(interp, this, format_args!("{}\n", e)); } // Read the subprocess back via the arena instead of holding `child_out` - // across the call. - let child: *mut ShellSubprocess = match &interp.as_cmd(this).exec { - Exec::Subproc(sub) => sub.child, + // across the call. `spawn_async` Ok ⇒ `sub.child` owns the subprocess. + match &mut interp.as_cmd_mut(this).exec { + Exec::Subproc(sub) => sub.child.as_mut().unwrap().r#ref(), _ => unreachable!(), - }; - // SAFETY: `spawn_async` Ok ⇒ wrote a live `heap::alloc` subprocess - // pointer into `*child_out` (== `sub.child`); valid until `Cmd::deinit` - // reclaims the box. Single-threaded. - let subproc = unsafe { &mut *child }; - subproc.r#ref(); + } drop(arena); if did_exit_immediately { - // `watch()` failed → process already gone. - let process = subproc.proc(); + // `watch()` failed → process already gone. Read out the `Process` + // pointer and end the subprocess borrow first: `on_exit`/`wait` + // re-enter the shell and re-borrow the child. + let process = match &mut interp.as_cmd_mut(this).exec { + Exec::Subproc(sub) => core::ptr::from_mut(sub.child.as_mut().unwrap().proc()), + _ => unreachable!(), + }; + // SAFETY: the `Process` allocation is owned by the child box, which + // outlives this frame; no other borrow of it is live here. + let process = unsafe { &mut *process }; if process.has_exited() { let status = process.status.clone(); process.on_exit(status, &crate::api::bun::process::rusage_zeroed()); @@ -778,9 +785,7 @@ impl Cmd { } } else if crate::webcore::ReadableStream::from_js(jsval, global)?.is_some() { panic!("TODO SHELL READABLE STREAM"); - } else if let Some(req) = jsval.as_::() { - // SAFETY: `as_` returns a live JSC-owned `*mut Response`. - let req = unsafe { &mut *req }; + } else if let Some(req) = jsval.as_class_ref::() { req.get_body_value().to_blob_if_possible(); if flags.stdin() { let b = req.get_body_value().use_as_any_blob(); @@ -887,23 +892,20 @@ impl Cmd { match core::mem::take(&mut me.exec) { Exec::None => {} Exec::Builtin(b) => drop(b), - Exec::Subproc(sub) if !sub.child.is_null() => { - // SAFETY: `child` was set by `initSubproc` from a - // `heap::alloc(ShellSubprocess)` and stays valid until this - // drop. Single-threaded. Reclaiming the box runs - // `ShellSubprocess::drop` → `finalize_sync` (closes stdio). - let mut child = unsafe { bun_core::heap::take(sub.child) }; - if !child.has_exited() { - let _ = child.try_kill(9); + Exec::Subproc(mut sub) => { + // Dropping the box runs `ShellSubprocess::drop` → `finalize_sync` + // (closes stdio). A `None` child means spawn failed before the + // subprocess was returned: nothing to tear down. + if let Some(mut child) = sub.child.take() { + if !child.has_exited() { + let _ = child.try_kill(9); + } + child.unref::(); + drop(child); } - child.unref::(); - drop(child); // `sub.buffered_closed` drops here, freeing any captured // `Vec`s (spec `buffered_closed.deinit()`). } - // `Exec::Subproc` with null `child`: spawn failed before the - // subprocess box was returned. Nothing to tear down. - Exec::Subproc(_) => {} } // Argv/env are heap-owned `Vec`s; there is no spawn arena to free. // `base.shell` is borrowed (or, when parent is Pipeline, freed by @@ -984,9 +986,9 @@ impl Cmd { let Exec::Subproc(sub) = &mut self.exec else { return; }; - // Raw deref keeps the borrow disjoint from `sub.buffered_closed` below. - // SAFETY: `child` is the live subprocess owned by this Cmd. - let child = unsafe { &mut *sub.child }; + let Some(child) = sub.child.as_deref_mut() else { + return; + }; // Tee into the JS-side captured buffer if stdout is an fd with a // `captured` slot and the redirect didn't send stdout elsewhere. if let IoOutKind::Fd(fd) = &self.io.stdout { @@ -1005,7 +1007,7 @@ impl Cmd { &mut child.stdout, matches!(self.io.stdout, IoOutKind::Pipe), redirect.redirects_elsewhere(ast::IoKind::Stdout), - self.base.shell_mut().buffered_stdout(), + self.base.shell().buffered_stdout().as_ptr(), ); child.close_io(StdioKind::Stdout); } @@ -1020,9 +1022,9 @@ impl Cmd { let Exec::Subproc(sub) = &mut self.exec else { return; }; - // Raw deref keeps the borrow disjoint from `sub.buffered_closed` below. - // SAFETY: `child` is the live subprocess owned by this Cmd. - let child = unsafe { &mut *sub.child }; + let Some(child) = sub.child.as_deref_mut() else { + return; + }; if let IoOutKind::Fd(fd) = &self.io.stderr { // SAFETY: single-threaded; the captured `Vec` lives in the // owning `ShellExecEnv` and no other borrow of it is live here. @@ -1039,7 +1041,7 @@ impl Cmd { &mut child.stderr, matches!(self.io.stderr, IoOutKind::Pipe), redirect.redirects_elsewhere(ast::IoKind::Stderr), - self.base.shell_mut().buffered_stderr(), + self.base.shell().buffered_stderr().as_ptr(), ); child.close_io(StdioKind::Stderr); } diff --git a/src/runtime/shell/states/CondExpr.rs b/src/runtime/shell/states/CondExpr.rs index 9b97b81232d3..d04b7e18d798 100644 --- a/src/runtime/shell/states/CondExpr.rs +++ b/src/runtime/shell/states/CondExpr.rs @@ -326,16 +326,12 @@ impl CondExpr { // No-IO path: append to the shell env's captured stderr and finish // synchronously with exit 1 (matches `on_io_writer_chunk`). if let OutKind::Pipe = &interp.as_condexpr(this).io.stderr { - // SAFETY: single trampoline frame; no other borrow of the env's - // (or its parent's) stderr buffer is live. - let stderr = unsafe { - interp - .as_condexpr_mut(this) - .base - .shell_mut() - .buffered_stderr_mut() - }; - stderr.extend_from_slice(&buf); + interp + .as_condexpr(this) + .base + .shell() + .buffered_stderr() + .with_mut(|stderr| stderr.extend_from_slice(&buf)); } let parent = interp.as_condexpr(this).base.parent; interp.child_done(parent, this, 1) diff --git a/src/runtime/shell/states/Expansion.rs b/src/runtime/shell/states/Expansion.rs index 26181fd97115..524d291768d2 100644 --- a/src/runtime/shell/states/Expansion.rs +++ b/src/runtime/shell/states/Expansion.rs @@ -4,6 +4,8 @@ //! needs to be evaluated at runtime — this state node walks the atom and //! produces zero or more output strings. +use bun_jsc::JsCell; + use crate::shell::ast; use crate::shell::interpreter::{ EventLoopHandle, Interpreter, Node, NodeId, ShellExecEnv, ShellExecEnvKind, StateKind, log, @@ -128,15 +130,11 @@ impl Expansion { /// `child_done` advances `word_idx`. pub fn next(interp: &Interpreter, this: NodeId) -> Yield { loop { - // Split-borrow: `me` from `nodes`, `vm_args_utf8` from its own - // field, so `expand_simple_no_io` can expand `$N` without aliasing. - // R-2: both are `JsCell`-backed; `as_ptr()`/`node_mut()` project - // disjoint `&mut` from `&Interpreter`. let event_loop = interp.event_loop; let command_ctx = interp.command_ctx; - // SAFETY: single-JS-thread; `vm_args_utf8` and `nodes` are - // disjoint `JsCell` fields (no aliasing between the two borrows). - let vm_args_utf8 = unsafe { &mut *interp.vm_args_utf8.as_ptr() }; + // Passed as `&JsCell` so the `&mut Vec` lives only inside the + // `with_mut` in `expand_simple_no_io`, not across child spawns. + let vm_args_utf8 = &interp.vm_args_utf8; let me = interp.as_expansion_mut(this); match me.state { ExpansionState::Idle => { @@ -174,7 +172,6 @@ impl Expansion { me.word_idx = 1; } - let shell_ptr: *mut ShellExecEnv = me.base.shell; while me.word_idx < atoms_len { let simple: &ast::SimpleAtom = match atom { ast::Atom::Simple(s) => s, @@ -211,9 +208,9 @@ impl Expansion { stdout: OutKind::Pipe, stderr: interp.root_io().stderr.clone(), }; - // SAFETY: `shell_ptr` is a live env owned by the parent state - // node and outlives this expansion. - let duped = match unsafe { &mut *shell_ptr } + let duped = match me + .base + .shell_mut() .dupe_for_subshell(&io, ShellExecEnvKind::CmdSubst) { Ok(d) => d, @@ -464,7 +461,7 @@ impl Expansion { expand_tilde: bool, event_loop: EventLoopHandle, command_ctx: *mut bun_options_types::context::ContextData, - vm_args_utf8: &mut Vec, + vm_args_utf8: &JsCell>, ) -> bool { use crate::shell::env_str::EnvStr; match atom { @@ -488,8 +485,11 @@ impl Expansion { } } ast::SimpleAtom::VarArgv(int) => { - // SAFETY: `command_ctx` is the live VM ctx; `vm_args_utf8` borrows it. - Interpreter::append_var_argv(out, *int, event_loop, command_ctx, vm_args_utf8); + // `command_ctx` is the live VM ctx. Nothing in `append_var_argv` + // reaches JS, so the `&mut Vec` cannot be re-entered here. + vm_args_utf8.with_mut(|v| { + Interpreter::append_var_argv(out, *int, event_loop, command_ctx, v); + }); } ast::SimpleAtom::Asterisk => { meta_offsets.push(out.len() as u32); @@ -597,16 +597,13 @@ impl Expansion { // Child is a Script (command substitution). Its captured stdout lives // in the duped `ShellExecEnv` it owns; read it before deinit. debug_assert!(matches!(interp.node(child).kind(), StateKind::Script)); - // SAFETY: single trampoline frame; the child script's env (and its - // parent buffer in the `Borrowed` case) has no other live borrow. - let stdout = unsafe { - interp - .as_script_mut(child) - .base - .shell_mut() - .buffered_stdout_mut() - } - .clone(); + let stdout = interp + .as_script(child) + .base + .shell() + .buffered_stdout() + .get() + .clone(); // Propagate the exit code if the *whole* atom was a single `$(...)` // (so `$(false)` as argv0 fails the command). diff --git a/src/runtime/shell/subproc.rs b/src/runtime/shell/subproc.rs index eca18206ee13..54369cfb4945 100644 --- a/src/runtime/shell/subproc.rs +++ b/src/runtime/shell/subproc.rs @@ -326,15 +326,22 @@ impl ShellSubprocess { pub const DEFAULT_MAX_BUFFER_SIZE: u32 = DEFAULT_MAX_BUFFER_SIZE; /// Borrow the intrusively ref-counted Process mutably. - /// SAFETY-internal: shell is single-threaded; `self.process` is non-null - /// for the lifetime of `ShellSubprocess` (set in `spawn_maybe_sync_impl`). #[inline] - #[allow(clippy::mut_from_ref)] - pub fn proc(&self) -> &mut Process { - // SAFETY: see doc comment. + pub fn proc(&mut self) -> &mut Process { + // SAFETY: `self.process` is non-null for the lifetime of + // `ShellSubprocess` (set in `spawn_maybe_sync_impl`); `&mut self` + // makes this the only live borrow of it. unsafe { &mut *self.process } } + /// Shared borrow of the process, for read-only queries. + #[inline] + pub fn proc_ref(&self) -> &Process { + // SAFETY: `self.process` is non-null for the lifetime of + // `ShellSubprocess` (set in `spawn_maybe_sync_impl`). + unsafe { &*self.process } + } + pub fn on_static_pipe_writer_done(&mut self) { log!( "Subproc(0x{:x}) onStaticPipeWriterDone(cmd={})", @@ -354,7 +361,7 @@ impl ShellSubprocess { } pub fn has_exited(&self) -> bool { - self.proc().has_exited() + self.proc_ref().has_exited() } pub fn r#ref(&mut self) { @@ -382,7 +389,7 @@ impl ShellSubprocess { } pub fn has_killed(&self) -> bool { - self.proc().has_killed() + self.proc_ref().has_killed() } pub fn try_kill(&mut self, sig: i32) -> bun_sys::Result<()> { @@ -513,17 +520,16 @@ impl ShellSubprocess { /// is uv-initialized depends on how far startWithCurrentPipe got, so a blind close or /// destroy is unsafe. Fall back to leaking the Subprocess (pre-existing behavior) /// rather than risk closing an uninitialized handle. - fn abort_after_failed_start(this: *mut Self) { + fn abort_after_failed_start(self: Box) { #[cfg(windows)] { - let _ = this; - return; + // Leak rather than drop: `ShellSubprocess::drop` would run the + // teardown the doc comment above forbids. + let _ = bun_core::heap::release(self); } #[cfg(not(windows))] { - // SAFETY: `this` was created via `heap::alloc` in `spawn` and is - // uniquely owned here; reclaim and tear down. - let mut subproc = unsafe { bun_core::heap::take(this) }; + let mut subproc = self; for r in [&mut subproc.stdout, &mut subproc.stderr] { if let Readable::Pipe(pipe) = r { // `start()` failed before any reader callback registered, @@ -866,7 +872,11 @@ impl ShellSubprocess { if let Err(err) = unsafe { buffer_mut(buffer) }.start() { let sys_err = err.to_shell_system_error(); let _ = subproc.try_kill(SignalCode::SIGTERM as i32); - Self::abort_after_failed_start(subprocess); + // Clear the out slot before reclaiming: it owns the subprocess. + // SAFETY: `out_subproc` is the caller's live, initialised slot. + unsafe { *out_subproc = core::ptr::null_mut() }; + // SAFETY: `subprocess` is the `Box` allocated above and is uniquely owned here. + Self::abort_after_failed_start(unsafe { bun_core::heap::take(subprocess) }); return Err(ShellErr::Sys(sys_err)); } } @@ -877,7 +887,11 @@ impl ShellSubprocess { { let sys_err = err.to_shell_system_error(); let _ = subproc.try_kill(SignalCode::SIGTERM as i32); - Self::abort_after_failed_start(subprocess); + // Clear the out slot before reclaiming: it owns the subprocess. + // SAFETY: `out_subproc` is the caller's live, initialised slot. + unsafe { *out_subproc = core::ptr::null_mut() }; + // SAFETY: `subprocess` is the `Box` allocated above and is uniquely owned here. + Self::abort_after_failed_start(unsafe { bun_core::heap::take(subprocess) }); return Err(ShellErr::Sys(sys_err)); } @@ -887,7 +901,11 @@ impl ShellSubprocess { { let sys_err = err.to_shell_system_error(); let _ = subproc.try_kill(SignalCode::SIGTERM as i32); - Self::abort_after_failed_start(subprocess); + // Clear the out slot before reclaiming: it owns the subprocess. + // SAFETY: `out_subproc` is the caller's live, initialised slot. + unsafe { *out_subproc = core::ptr::null_mut() }; + // SAFETY: `subprocess` is the `Box` allocated above and is uniquely owned here. + Self::abort_after_failed_start(unsafe { bun_core::heap::take(subprocess) }); return Err(ShellErr::Sys(sys_err)); } @@ -1545,11 +1563,11 @@ impl<'a> SpawnArgs<'a> { // has no PATH). PATH="" (explicit empty) is preserved — that's a // deliberate "search nothing" and substituting a default would // change argv[0] resolution on existing platforms. - // SAFETY: `event_loop.env()` returns the long-lived `*mut Loader` + // SAFETY: `event_loop.env()` back-references the long-lived Loader // owned by the VM (valid for the lifetime of the spawn args), and // `BUN_DEFAULT_PATH_FOR_SPAWN` is a NUL-terminated C-string constant. path: unsafe { - if let Some(p) = (*event_loop.env()).get(b"PATH") { + if let Some(p) = (*event_loop.env().as_ptr()).get(b"PATH") { p } else if cfg!(unix) { core::ffi::CStr::from_ptr(BUN_DEFAULT_PATH_FOR_SPAWN).to_bytes() diff --git a/src/runtime/socket/Listener.rs b/src/runtime/socket/Listener.rs index 7f19e2ccdf49..0afc90b2d9db 100644 --- a/src/runtime/socket/Listener.rs +++ b/src/runtime/socket/Listener.rs @@ -56,8 +56,8 @@ fn with_ssl_ctx_cache( ); // SAFETY: `state` is the per-thread `RuntimeState` boxed in // `init_runtime_state`, address-stable until VM teardown, and only the JS - // thread reaches here — so this `&mut` is unique for `f`'s duration. - f(unsafe { &mut (*state).ssl_ctx_cache }) + // thread reaches here. + unsafe { (*state).ssl_ctx_cache.with_mut(f) } } // Route through the codegen'd `toJS` wrapper so we @@ -77,7 +77,7 @@ pub struct Listener { pub listener: Cell, pub poll_ref: JsCell, - pub connection: UnixOrHost, + pub connection: JsCell, /// Embedded sweep/iteration list-head for every accepted socket on this /// listener. `group.ext` = `*Listener`, so the dispatch handler recovers us /// from the socket without a context-ext lookup. @@ -85,7 +85,7 @@ pub struct Listener { /// `SSL_CTX*` for accepted sockets. One owned ref; `SSL_CTX_free` on close. /// `SSL_new()` per-accept takes its own ref, so accepted sockets outlive a /// stopped listener safely. - pub secure_ctx: Option>, + pub secure_ctx: Cell>>, pub ssl: bool, pub protos: Option>, @@ -219,20 +219,22 @@ impl Listener { let default_data = socket_config.default_data; let ssl_cfg_taken = socket_config.ssl.take(); - let this: *mut Listener = bun_core::heap::into_raw(Box::new(Listener { + // `release` = `Box::leak`; ownership moves to the JS wrapper below, + // or back through `heap::take` on the error path. + let this_owned: &mut Listener = bun_core::heap::release(Box::new(Listener { handlers, - connection, + connection: JsCell::new(connection), ssl: ssl_enabled, listener: Cell::new(ListenerType::None), protos: protos_taken, poll_ref: JsCell::new(KeepAlive::init()), group: JsCell::new(uws::SocketGroup::default()), - secure_ctx: None, + secure_ctx: Cell::new(None), strong_data: JsCell::new(Strong::empty()), this_value: JsCell::new(JsRef::empty()), })); - // SAFETY: just allocated, non-null, exclusive - let this_ref = unsafe { &mut *this }; + let this: *mut Listener = &raw mut *this_owned; + let this_ref: &Listener = this_owned; if !default_data.is_empty() { this_ref .strong_data @@ -258,8 +260,8 @@ impl Listener { } Err(_) => { this_ref.strong_data.with_mut(|s| s.deinit()); - // SAFETY: reclaim the Box we leaked via into_raw; drops connection, - // protos, and the handlers `Rc`. + // SAFETY: reclaim the Box handed off via `release`; drops + // connection, protos, and the handlers `Rc`. drop(unsafe { bun_core::heap::take(this) }); return Err(global.throw_invalid_arguments(format_args!( "Failed to listen at {}", @@ -297,22 +299,24 @@ impl Listener { let fd_opt = socket_config.fd; let ssl_cfg_taken = socket_config.ssl.take(); - let this: *mut Listener = bun_core::heap::into_raw(Box::new(Listener { + // `release` = `Box::leak`; ownership moves to the JS wrapper below, or back + // through the cleanup guard on the error paths. + let this_owned: &mut Listener = bun_core::heap::release(Box::new(Listener { handlers, - // Placeholder until `this_ref.connection = connection` below. + // Placeholder until `this_ref.connection.set(connection)` below. // Cannot `mem::zeroed()` a Rust enum (UB). - connection: UnixOrHost::Fd(Fd::invalid()), + connection: JsCell::new(UnixOrHost::Fd(Fd::invalid())), ssl: ssl_enabled, protos: protos_taken, listener: Cell::new(ListenerType::None), poll_ref: JsCell::new(KeepAlive::init()), group: JsCell::new(uws::SocketGroup::default()), - secure_ctx: None, + secure_ctx: Cell::new(None), strong_data: JsCell::new(Strong::empty()), this_value: JsCell::new(JsRef::empty()), })); - // SAFETY: just allocated, non-null, exclusive - let this_ref = unsafe { &mut *this }; + let this: *mut Listener = &raw mut *this_owned; + let this_ref: &Listener = this_owned; this_ref .group .with_mut(|g| g.init(uws::Loop::get(), None, this.cast::())); @@ -329,26 +333,27 @@ impl Listener { // Disarmed via `into_inner` once ownership transfers to the JS wrapper. let cleanup = scopeguard::guard(this, |this| { // SAFETY: this is still the sole owner on the error path - let this_ref = unsafe { &mut *this }; - if let Some(c) = this_ref.secure_ctx { + let this_owned: Box = unsafe { bun_core::heap::take(this) }; + if let Some(c) = this_owned.secure_ctx.get() { // SAFETY: FFI — secure_ctx holds one owned SSL_CTX ref from create_ssl_context unsafe { boring_sys::SSL_CTX_free(c.as_ptr()) }; } // protos: Box drops automatically when Listener is dropped below bun_core::asan::unregister_root_region( - this_ref.group.as_ptr().cast::(), + this_owned.group.as_ptr().cast::(), size_of::(), ); // SAFETY: group was init'd above; not concurrently walked. - unsafe { uws::SocketGroup::destroy(this_ref.group.as_ptr()) }; - // SAFETY: reclaim the Box we leaked via into_raw - drop(unsafe { bun_core::heap::take(this) }); + unsafe { uws::SocketGroup::destroy(this_owned.group.as_ptr()) }; + drop(this_owned); }); if let Some(ssl_cfg) = ssl_cfg_taken.as_ref() { let mut create_err = uws::create_bun_socket_error_t::none; match ssl_cfg.as_usockets().create_ssl_context(&mut create_err) { - Some(ctx) => this_ref.secure_ctx = NonNull::new(ctx.cast::()), + Some(ctx) => this_ref + .secure_ctx + .set(NonNull::new(ctx.cast::())), None => { return Err(global.throw_value( crate::socket::uws_jsc::create_bun_socket_error_to_js(create_err, global), @@ -377,6 +382,7 @@ impl Listener { let secure_ctx_ptr: Option<*mut uws::SslCtx> = this_ref .secure_ctx + .get() .map(|p| p.as_ptr().cast::()); let mut errno: c_int = 0; @@ -473,7 +479,7 @@ impl Listener { return Err(global.throw_value(err)); } - this_ref.connection = connection; + this_ref.connection.set(connection); this_ref.listener.set(ListenerType::Uws(listen_socket)); if !default_data.is_empty() { this_ref @@ -483,7 +489,7 @@ impl Listener { if let Some(ssl_config) = ssl_cfg_taken.as_ref() { // `ssl_enabled` ⇒ `createSSLContext` succeeded above ⇒ `secure_ctx` set. - let secure = this_ref.secure_ctx.expect("unreachable"); + let secure = this_ref.secure_ctx.get().expect("unreachable"); if let Some(server_name) = ssl_config.server_name_cstr() { if !server_name.to_bytes().is_empty() { // Registering the default cert under its own server_name is a @@ -791,15 +797,13 @@ impl Listener { ListenerType::NamedPipe(_) => {} ListenerType::None => {} } - // `deinit` frees the allocation itself (`heap::take`); hand ownership - // back so its existing raw-ptr teardown path stays intact. - Self::deinit(Box::into_raw(self)); + Self::deinit(self); } /// Match Node.js/libuv: unlink the unix socket file before closing the listening fd. /// Unlinking after close would race with another process creating a socket at the same path. fn unlink_unix_socket_path(this: &Self) { - let UnixOrHost::Unix(path) = &this.connection else { + let UnixOrHost::Unix(path) = this.connection.get() else { return; }; // Abstract sockets (Linux) start with a NUL byte and have no filesystem entry. @@ -810,36 +814,34 @@ impl Listener { let _ = bun_sys::unlink(bun_paths::resolve_path::z(path, &mut buf)); } - fn deinit(this: *mut Self) { + // `boxed_local`: the `Box` is the ownership unit being reclaimed here. + #[allow(clippy::boxed_local)] + fn deinit(self: Box) { log!("deinit"); - // SAFETY: `this` is a Box leaked via into_raw; sole owner here - let this_ref = unsafe { &mut *this }; - this_ref.this_value.with_mut(|r| r.finalize()); - this_ref.strong_data.with_mut(|s| s.deinit()); - this_ref.poll_ref.with_mut(|p| p.unref(bun_io::js_vm_ctx())); - debug_assert!(matches!(this_ref.listener.get(), ListenerType::None)); + self.this_value.with_mut(|r| r.finalize()); + self.strong_data.with_mut(|s| s.deinit()); + self.poll_ref.with_mut(|p| p.unref(bun_io::js_vm_ctx())); + debug_assert!(matches!(self.listener.get(), ListenerType::None)); // Clear the back-pointer before force-closing: this listener is already // releasing its own `poll_ref`/`this_value`, so an accepted socket's // `on_close` must not reach back in and release them a second time. - this_ref.handlers.set_listener(None); - if this_ref.handlers.active_connections.get() > 0 { - this_ref.group.with_mut(|g| g.close_all()); + self.handlers.set_listener(None); + if self.handlers.active_connections.get() > 0 { + self.group.with_mut(|g| g.close_all()); } bun_core::asan::unregister_root_region( - this_ref.group.as_ptr().cast::(), + self.group.as_ptr().cast::(), size_of::(), ); // SAFETY: group was init'd in listen(); not concurrently walked. - unsafe { uws::SocketGroup::destroy(this_ref.group.as_ptr()) }; - if let Some(ctx) = this_ref.secure_ctx { + unsafe { uws::SocketGroup::destroy(self.group.as_ptr()) }; + if let Some(ctx) = self.secure_ctx.get() { // SAFETY: FFI — secure_ctx holds one owned SSL_CTX ref; release it unsafe { boring_sys::SSL_CTX_free(ctx.as_ptr()) }; } - // connection / protos / the handlers `Rc`: dropped by heap::take below - // SAFETY: reclaim the Box allocated in listen() - drop(unsafe { bun_core::heap::take(this) }); + // connection / protos / the handlers `Rc`: dropped with the Box below } #[bun_jsc::host_fn(getter)] @@ -849,7 +851,7 @@ impl Listener { #[bun_jsc::host_fn(getter)] pub fn get_unix(this: &Self, global: &JSGlobalObject) -> JSValue { - let UnixOrHost::Unix(unix) = &this.connection else { + let UnixOrHost::Unix(unix) = this.connection.get() else { return JSValue::UNDEFINED; }; ZigString::init(unix).with_encoding().to_js(global) @@ -857,7 +859,7 @@ impl Listener { #[bun_jsc::host_fn(getter)] pub fn get_hostname(this: &Self, global: &JSGlobalObject) -> JSValue { - let UnixOrHost::Host { host, .. } = &this.connection else { + let UnixOrHost::Host { host, .. } = this.connection.get() else { return JSValue::UNDEFINED; }; ZigString::init(host).with_encoding().to_js(global) @@ -865,7 +867,7 @@ impl Listener { #[bun_jsc::host_fn(getter)] pub fn get_port(this: &Self, _global: &JSGlobalObject) -> JSValue { - let UnixOrHost::Host { port, .. } = &this.connection else { + let UnixOrHost::Host { port, .. } = this.connection.get() else { return JSValue::UNDEFINED; }; JSValue::js_number(*port as f64) @@ -1575,32 +1577,29 @@ pub struct WindowsNamedPipeListeningContext { #[cfg(windows)] impl WindowsNamedPipeListeningContext { - fn on_client_connect(this: *mut Self, status: uv::ReturnCode) { - // SAFETY: `this` is the `data` pointer libuv hands back; it was set to a - // live heap `WindowsNamedPipeListeningContext` in `listen_named_pipe`. - let this_ref = unsafe { &mut *this }; - let shutting_down = this_ref.vm.is_shutting_down(); - if status != uv::ReturnCode::ZERO || shutting_down || this_ref.listener.is_none() { + fn on_client_connect(&mut self, status: uv::ReturnCode) { + let shutting_down = self.vm.is_shutting_down(); + if status != uv::ReturnCode::ZERO || shutting_down || self.listener.is_none() { // connection dropped or vm is shutting down or we are deiniting/closing return; } // `BackRef` deref — owner `Listener` outlives this context (see field doc). - let listener_ref = this_ref.listener.unwrap(); + let listener_ref = self.listener.unwrap(); let listener: &Listener = listener_ref.get(); use crate::socket::windows_named_pipe_context::SocketType as PipeSocketType; - let socket: PipeSocketType = if this_ref.ctx.is_some() { + let socket: PipeSocketType = if self.ctx.is_some() { PipeSocketType::Tls(Listener::on_name_pipe_created::(listener)) } else { PipeSocketType::Tcp(Listener::on_name_pipe_created::(listener)) }; - let client = WindowsNamedPipeContext::create(&this_ref.global_this, socket); + let client = WindowsNamedPipeContext::create(&self.global_this, socket); // SAFETY: `client` was just heap-allocated by `create()`; exclusive here. let result = unsafe { (*client) .named_pipe - .get_accepted_by(&mut this_ref.uv_pipe, this_ref.ctx.map(|p| p.as_ptr())) + .get_accepted_by(&mut self.uv_pipe, self.ctx.map(|p| p.as_ptr())) }; if result.is_err() { // connection dropped @@ -1621,17 +1620,21 @@ impl WindowsNamedPipeListeningContext { /// explicitly — matches the `extern "C" fn` callback convention used in /// `udp_socket.rs` / `bun_io::PipeReader`. extern "C" fn uv_on_client_connect(handle: *mut uv::uv_stream_t, status: uv::ReturnCode) { - // SAFETY: `data` was set to `*mut Self` by `Pipe::listen` below. - let this = unsafe { (*handle).data.cast::() }; - Self::on_client_connect(this, status); + // SAFETY: `data` was set to `*mut Self` by `Pipe::listen` below; libuv only + // dispatches this callback while that allocation is live. + let this = unsafe { &mut *(*handle).data.cast::() }; + this.on_client_connect(status); } /// `uv_close_cb` trampoline. Only ever invoked by libuv (coerces to the /// `uv_close_cb` fn-pointer type at the `Pipe::close` call site); body /// wraps its deref explicitly. extern "C" fn on_pipe_closed(pipe: *mut uv::Pipe) { - // SAFETY: `pipe.data` was set to `this` in `close_pipe_and_deinit`. - let this = unsafe { (*pipe).data.cast::() }; + // SAFETY: `pipe.data` was set to `this` in `close_pipe_and_deinit`, the unique + // owner; `uv_close` has completed so the loop no longer references it. + let this = unsafe { + bun_core::heap::take((*pipe).data.cast::()) + }; Self::deinit(this); } @@ -1656,15 +1659,17 @@ impl WindowsNamedPipeListeningContext { ) -> Result<*mut WindowsNamedPipeListeningContext, bun_core::Error> { // Heap-allocate at the final address so libuv can // store a pointer back into `uv_pipe`. - let this = bun_core::heap::into_raw(Box::new(WindowsNamedPipeListeningContext { - uv_pipe: bun_core::ffi::zeroed(), - listener: NonNull::new(listener).map(bun_ptr::BackRef::from), - global_this: GlobalRef::from(global_this), - vm: global_this.bun_vm(), - ctx: None, - })); - // SAFETY: just allocated, non-null, exclusive. - let this_ref = unsafe { &mut *this }; + // `release` = `Box::leak`; ownership moves to the returned pointer, reclaimed + // by the cleanup guard or by `on_pipe_closed` -> `deinit`. + let this_ref: &mut WindowsNamedPipeListeningContext = + bun_core::heap::release(Box::new(WindowsNamedPipeListeningContext { + uv_pipe: bun_core::ffi::zeroed(), + listener: NonNull::new(listener).map(bun_ptr::BackRef::from), + global_this: GlobalRef::from(global_this), + vm: global_this.bun_vm(), + ctx: None, + })); + let this: *mut WindowsNamedPipeListeningContext = &raw mut *this_ref; // Cleanup guard: once the uv pipe handle is registered with the loop it must be closed via // uv_close; before that point we can free the struct directly. `deinit()` also @@ -1675,7 +1680,8 @@ impl WindowsNamedPipeListeningContext { // SAFETY: pipe is registered with the loop; close → on_pipe_closed → deinit. unsafe { Self::close_pipe_and_deinit(this) }; } else { - Self::deinit(this); + // SAFETY: `into_raw`'d above and never registered with the loop. + Self::deinit(unsafe { bun_core::heap::take(this) }); } }); @@ -1731,14 +1737,11 @@ impl WindowsNamedPipeListeningContext { Ok(this) } - fn deinit(this: *mut Self) { - // SAFETY: `this` is a live `heap::alloc` allocation; this is the last owner. - unsafe { - (*this).listener = None; - if let Some(ctx) = (*this).ctx.take() { - boring_sys::SSL_CTX_free(ctx.as_ptr()); - } - drop(bun_core::heap::take(this)); + fn deinit(mut self: Box) { + self.listener = None; + if let Some(ctx) = self.ctx.take() { + // SAFETY: the server owns the only reference to this context. + unsafe { boring_sys::SSL_CTX_free(ctx.as_ptr()) }; } } } diff --git a/src/runtime/socket/WindowsNamedPipeContext.rs b/src/runtime/socket/WindowsNamedPipeContext.rs index c421fa568fb7..8084ebbb88b2 100644 --- a/src/runtime/socket/WindowsNamedPipeContext.rs +++ b/src/runtime/socket/WindowsNamedPipeContext.rs @@ -283,16 +283,9 @@ impl WindowsNamedPipeContext { } #[cfg(windows)] - fn run_event(this: *mut Self) { - // SAFETY: called from AnyTask; `this` is the live ctx pointer registered in create() - match unsafe { (*this).task_event } { - EventState::Deinit => { - // SAFETY: `this` was allocated via heap::alloc in create(); refcount hit zero - // and this deferred task is the sole remaining owner. Drop runs field destructors. - drop(unsafe { bun_core::heap::take(this) }); - } - EventState::None => panic!("Invalid event state"), - } + fn run_event(this: Box) { + assert!(this.task_event == EventState::Deinit, "Invalid event state"); + // `this` drops here: `Drop` derefs the socket, then field destructors run. } /// Owns the freshly-`create()`d context until `disarm()`: on any early @@ -370,7 +363,11 @@ impl WindowsNamedPipeContext { let task = AnyTask { ctx: ptr::NonNull::new(this.cast::()), callback: |ctx| { - Self::run_event(ctx.cast::()); + // SAFETY: `ctx` is the `heap::into_raw` allocation above; the refcount hit + // zero before `schedule_deinit` queued this task, so it is the sole owner. + Self::run_event(unsafe { + bun_core::heap::take(ctx.cast::()) + }); Ok(()) }, }; diff --git a/src/runtime/socket/socket_body.rs b/src/runtime/socket/socket_body.rs index 87aeb928f003..a2e71d26d3b5 100644 --- a/src/runtime/socket/socket_body.rs +++ b/src/runtime/socket/socket_body.rs @@ -3280,9 +3280,9 @@ impl NewSocket { debug_assert!(!state.is_null(), "RuntimeState not installed"); // SAFETY: per-thread `RuntimeState` boxed by `init_runtime_state`; // stable address for the VM's lifetime, JS-thread-only access. - unsafe { &mut (*state).ssl_ctx_cache } + unsafe { &(*state).ssl_ctx_cache } }; - owned_ctx = match cache.get_or_create(cfg, &mut create_err) { + owned_ctx = match cache.with_mut(|c| c.get_or_create(cfg, &mut create_err)) { // SAFETY: `get_or_create` hands back a +1 ref. Some(c) => unsafe { boringssl_sys::OwnedSslCtx::from_raw(c.cast::()) }, None => { @@ -4039,8 +4039,8 @@ impl DuplexUpgradeContext { // // SAFETY: `this` is live; short-lived `&` for the null-check. if unsafe { (*this).tls.is_none() } { - // SAFETY: per fn contract; no `&Self` live across this. - unsafe { Self::deinit(this) }; + // SAFETY: per fn contract — `this` is the unique owner. + Self::deinit(unsafe { bun_core::heap::take(this) }); return; } let started: Result<(), bun_core::Error> = { @@ -4082,8 +4082,8 @@ impl DuplexUpgradeContext { // was never registered and nothing will schedule // `.Close`. Same as the `tls == null` early-return // above: tear down here. - // SAFETY: per fn contract; no `&Self` live across this. - unsafe { Self::deinit(this) }; + // SAFETY: per fn contract — `this` is the unique owner. + Self::deinit(unsafe { bun_core::heap::take(this) }); return; } // SAFETY: `this` is live; short-lived `&mut` for the field write. @@ -4092,8 +4092,8 @@ impl DuplexUpgradeContext { // Previously this only called `upgrade.close()` and never `deinit`, // leaking the SSLWrapper, the strong refs, and this struct itself // for every duplex-upgraded TLS socket. - // SAFETY: per fn contract; no `&Self` live across this. - EventState::Close => unsafe { Self::deinit(this) }, + // SAFETY: per fn contract — `this` is the unique owner. + EventState::Close => Self::deinit(unsafe { bun_core::heap::take(this) }), } } @@ -4118,34 +4118,21 @@ impl DuplexUpgradeContext { self.enqueue_self_task(); } - /// # Safety - /// `this` must be the unique live pointer to the heap allocation produced - /// in `js_upgrade_duplex_to_tls`. Frees the allocation; callers must not - /// hold a `&`/`&mut Self` across this call (taking `&mut self` here would - /// be a Stacked Borrows protector violation when the backing `Box` is - /// reclaimed below). - unsafe fn deinit(this: *mut Self) { - { - // SAFETY: `this` is live; short-lived `&mut` ends before the - // `heap::take` free below — no protector spans the dealloc. - let this_ref = unsafe { &mut *this }; - if let Some(tls) = this_ref.tls.take() { - // Release the owner's +1. - tls.deref(); - } - // Close raced ahead of StartTLS — drop the unconsumed config. - this_ref.ssl_config = None; - if let Some(ctx) = this_ref.owned_ctx.take() { - // SAFETY: BoringSSL FFI; we hold one owned ref. - unsafe { boringssl_sys::SSL_CTX_free(ctx) }; - } + /// Consumes the allocation from `js_upgrade_duplex_to_tls`; `UpgradedDuplex` + /// cleanup runs via its `Drop` when the `Box` is reclaimed at end of scope. + // `boxed_local`: the `Box` is the ownership unit being reclaimed here. + #[allow(clippy::boxed_local)] + fn deinit(mut self: Box) { + if let Some(tls) = self.tls.take() { + // Release the owner's +1. + tls.deref(); + } + // Close raced ahead of StartTLS — drop the unconsumed config. + self.ssl_config = None; + if let Some(ctx) = self.owned_ctx.take() { + // SAFETY: BoringSSL FFI; we hold one owned ref. + unsafe { boringssl_sys::SSL_CTX_free(ctx) }; } - // `UpgradedDuplex` cleanup - // runs via `Drop` when `heap::take(this)` frees the containing - // struct below; an explicit call here would double-free. - // SAFETY: heap-allocated in `js_upgrade_duplex_to_tls`; this is the - // matching free. No `&`/`&mut Self` survives past this point. - drop(unsafe { bun_core::heap::take(this) }); } } diff --git a/src/runtime/socket/udp_socket.rs b/src/runtime/socket/udp_socket.rs index 19496c6ac5fa..72c606d90ad1 100644 --- a/src/runtime/socket/udp_socket.rs +++ b/src/runtime/socket/udp_socket.rs @@ -138,8 +138,7 @@ extern "C" fn on_data( } let global_this = udp_socket.global_this.get(); - // SAFETY: buf valid for the duration of this callback per uws contract. - let buf = unsafe { &mut *buf }; + let buf = uws::udp::PacketBuffer::opaque_mut(buf); let mut i: c_int = 0; while i < packets { @@ -1138,15 +1137,10 @@ impl UDPSocket { callframe: &'a CallFrame, result: JsResult, } - extern "C" fn run(ctx: *mut Ctx<'_>, payload_roots: *mut MarkedArgumentBuffer) { - // SAFETY: ctx points to the stack-local Ctx passed to - // MarkedArgumentBuffer::run below; exclusive for this call. - let ctx = unsafe { &mut *ctx }; - // SAFETY: payload_roots is the stack MarkedArgumentBuffer that - // MarkedArgumentBuffer::run lends exclusively to this callback. - let payload_roots = unsafe { &mut *payload_roots }; - ctx.result = - UDPSocket::send_many_impl(ctx.this, ctx.global_this, ctx.callframe, payload_roots); + extern "C" fn run(ctx: &mut Ctx<'_>, payload_roots: &mut MarkedArgumentBuffer) { + let (this, global_this, callframe) = (ctx.this, ctx.global_this, ctx.callframe); + let result = UDPSocket::send_many_impl(this, global_this, callframe, payload_roots); + ctx.result = result; } let mut ctx = Ctx { this, diff --git a/src/runtime/test_runner/Collection.rs b/src/runtime/test_runner/Collection.rs index e249a0f5a337..96a4342eba6f 100644 --- a/src/runtime/test_runner/Collection.rs +++ b/src/runtime/test_runner/Collection.rs @@ -1,6 +1,8 @@ //! for the collection phase of test execution where we discover all the test() calls +use core::cell::Cell; use core::ptr::NonNull; +use bun_ptr::ParentRef; use crate::test_runner::expect::make_formatter; use bun_jsc::{DeprecatedStrong, JSGlobalObject, JSValue, JsResult}; @@ -65,12 +67,13 @@ impl Collection { }; let mut root_scope = DescribeScope::create(bun_test::BaseScope { - parent: Some(&raw mut *bun_test_root.hook_scope), + // SAFETY: `hook_scope` is owned by `BunTestRoot`, which outlives the collection tree. + parent: Some(unsafe { ParentRef::from_raw_mut(&raw mut *bun_test_root.hook_scope) }), name: None, concurrent: false, mode: bun_test::ScopeMode::Normal, - only, - has_callback: false, + only: Cell::new(only), + has_callback: Cell::new(false), test_id_for_debugger: 0, line_no: 0, }); diff --git a/src/runtime/test_runner/Execution.rs b/src/runtime/test_runner/Execution.rs index 6d2d50950df7..c8635c3275ca 100644 --- a/src/runtime/test_runner/Execution.rs +++ b/src/runtime/test_runner/Execution.rs @@ -347,9 +347,6 @@ impl Execution { data: &RefDataValue, ) -> JsResult { let _g = group_begin!(); - let buntest = buntest_strong.get(); - let buntest_ptr = NonNull::from(&mut *buntest); - let this = &mut buntest.execution; let mut now = Timespec::now_force_real_time(); match data { @@ -362,8 +359,10 @@ impl Execution { // step the sequence // if the group is complete, step the group - let Some((sequence_ptr, group_ptr)) = - this.get_current_and_valid_execution_sequence(data) + let Some((seq_abs, group_index)) = buntest_strong + .get() + .execution + .get_current_and_valid_execution_sequence(data) else { group_log::log(format_args!( "runOneCompleted: the data is outdated, invalid, or did not know the sequence", @@ -376,12 +375,13 @@ impl Execution { _ => unreachable!(), }; - // SAFETY: sequence_ptr points into this.sequences; valid while BunTest is alive. - debug_assert!(unsafe { sequence_ptr.as_ref() }.active_entry.is_some()); - Execution::advance_sequence(buntest_ptr, sequence_ptr, group_ptr); + debug_assert!( + buntest_strong.get().execution.sequences[seq_abs].active_entry.is_some() + ); + Execution::advance_sequence(buntest_strong, seq_abs, group_index); let sequence_result = - step_sequence(buntest_strong, global_this, group_ptr, sequence_index, &mut now)?; + step_sequence(buntest_strong, global_this, group_index, sequence_index, &mut now)?; match sequence_result { AdvanceSequenceStatus::Done => {} AdvanceSequenceStatus::Execute { timeout } => { @@ -389,11 +389,10 @@ impl Execution { } } // this sequence is complete; execute the next sequence - // re-slice from `this` each iteration via the group's range; carry - // `group` as NonNull so no `&mut ConcurrentGroup` aliases `&mut Execution`. + // re-derive the group each iteration: `step_sequence` can re-enter through JS. loop { - // SAFETY: group_ptr points into this.groups (disjoint from this.sequences). - let group = unsafe { &mut *group_ptr.as_ptr() }; + let this = &mut buntest_strong.get().execution; + let group = &this.groups[group_index]; let seq_len = group.sequence_end - group.sequence_start; if group.next_sequence_index >= seq_len { break; @@ -401,15 +400,14 @@ impl Execution { let next_idx = group.next_sequence_index; let abs_idx = group.sequence_start + next_idx; if this.sequences[abs_idx].executing { - group.next_sequence_index += 1; + this.groups[group_index].next_sequence_index += 1; continue; } let sequence_status = - step_sequence(buntest_strong, global_this, group_ptr, next_idx, &mut now)?; + step_sequence(buntest_strong, global_this, group_index, next_idx, &mut now)?; match sequence_status { AdvanceSequenceStatus::Done => { - // SAFETY: see above - unsafe { &mut *group_ptr.as_ptr() }.next_sequence_index += 1; + buntest_strong.get().execution.groups[group_index].next_sequence_index += 1; continue; } AdvanceSequenceStatus::Execute { timeout } => { @@ -418,8 +416,9 @@ impl Execution { } } // all sequences have started - // SAFETY: see above - if unsafe { group_ptr.as_ref() }.remaining_incomplete_entries == 0 { + if buntest_strong.get().execution.groups[group_index].remaining_incomplete_entries + == 0 + { return step_group(buntest_strong, global_this, &mut now); } return Ok(StepResult::Waiting { timeout: Timespec::EPOCH }); @@ -443,13 +442,12 @@ impl Execution { Some(&self.groups[self.group_index]) } - /// Returns `NonNull` pointers (not `&mut`) into `self.sequences` / `self.groups` so the - /// caller can hold both alongside other borrows of `self` without aliased-`&mut` UB. - /// Dereference at point-of-use only. + /// Returns `(absolute sequence index, group index)` into `self.sequences` / `self.groups`. + /// The caller re-derives the element borrow at point-of-use, so nothing aliases `self`. pub fn get_current_and_valid_execution_sequence( - &mut self, + &self, data: &RefDataValue, - ) -> Option<(NonNull, NonNull)> { + ) -> Option<(usize, usize)> { let _g = group_begin!(); group_log::log(format_args!("runOneCompleted: data: {}", data)); @@ -475,14 +473,13 @@ impl Execution { group_log::log(format_args!("runOneCompleted: the data did not know the group")); return None; } - // Disjoint split-borrow of `self.groups` and `self.sequences`. - let group = &mut self.groups[*group_index]; + let group = &self.groups[*group_index]; let seq_abs = group.sequence_start + entry_data.sequence_index; if seq_abs >= group.sequence_end { group_log::log(format_args!("runOneCompleted: the data did not know the sequence")); return None; } - let sequence = &mut self.sequences[seq_abs]; + let sequence = &self.sequences[seq_abs]; if i64::from(sequence.remaining_repeat_count) != entry_data.remaining_repeat_count { group_log::log(format_args!( "runOneCompleted: the data is for a previous repeat count (outdated)", @@ -500,21 +497,15 @@ impl Execution { return None; } group_log::log(format_args!("runOneCompleted: the data is valid and current")); - Some((NonNull::from(sequence), NonNull::from(group))) + Some((seq_abs, *group_index)) } - /// `sequence` / `group` are carried as `NonNull` (raw-pointer semantics) because they point into - /// `buntest.execution.{sequences,groups}` and would otherwise alias any live `&mut Execution`. - fn advance_sequence( - buntest: NonNull, - sequence_ptr: NonNull, - group_ptr: NonNull, - ) { + /// `seq_abs` / `group_index` index into `buntest.execution.{sequences,groups}`; each element + /// borrow is re-derived at point-of-use so none spans a call that can re-enter from JS. + fn advance_sequence(buntest_strong: &BunTestPtr, seq_abs: usize, group_index: usize) { let _g = group_begin!(); - // SAFETY: sequence_ptr / group_ptr point into disjoint fields of `buntest.execution` - // (`sequences` vs `groups`); no `&mut Execution` is live in this scope. - let sequence = unsafe { &mut *sequence_ptr.as_ptr() }; + let sequence = &mut buntest_strong.get().execution.sequences[seq_abs]; debug_assert!(sequence.executing); if let Some(entry_ptr) = sequence.active_entry { @@ -569,11 +560,10 @@ impl Execution { } // Only report the final result after all retries/repeats are done - Execution::on_sequence_completed(buntest, sequence); + Execution::on_sequence_completed(buntest_strong, seq_abs); // No more retries or repeats; mark sequence as complete - // SAFETY: group_ptr points into `buntest.execution.groups`, disjoint from `sequence`. - let group = unsafe { &mut *group_ptr.as_ptr() }; + let group = &mut buntest_strong.get().execution.groups[group_index]; if group.remaining_incomplete_entries == 0 { debug_assert!(false); // remaining_incomplete_entries should never go below 0 return; @@ -642,7 +632,8 @@ impl Execution { fn on_entry_completed(_entry: NonNull) {} - fn on_sequence_completed(buntest: NonNull, sequence: &mut ExecutionSequence) { + fn on_sequence_completed(buntest_strong: &BunTestPtr, seq_abs: usize) { + let sequence = &mut buntest_strong.get().execution.sequences[seq_abs]; let elapsed_ns: u64 = if sequence.started_at.eql(&Timespec::EPOCH) { 0 } else { @@ -672,22 +663,25 @@ impl Execution { _ => Result::Pass, }; } - if let Some(first_entry) = sequence.first_entry { - if sequence.test_entry.is_some() || sequence.result != Result::Pass { - // SAFETY: deref parent BunTest at point-of-use. `sequence` aliases - // `buntest.execution.sequences[i]`; `handle_test_completed`'s signature still takes - // both `&mut BunTest` and `&mut ExecutionSequence` (callee could be reshaped to avoid this). + let first_entry = sequence.first_entry; + let test_entry = sequence.test_entry; + let result = sequence.result; + + if let Some(first_entry) = first_entry { + if test_entry.is_some() || result != Result::Pass { + // Shared borrows only: `&BunTest` and the sequence it owns nest without aliasing. + let buntest: &BunTest = &***buntest_strong; test_command::CommandLineReporter::handle_test_completed( - unsafe { &mut *buntest.as_ptr() }, - sequence, + buntest, + &buntest.execution.sequences[seq_abs], // SAFETY: arena-owned entry, alive for lifetime of BunTest - unsafe { &mut *sequence.test_entry.unwrap_or(first_entry).as_ptr() }, + unsafe { test_entry.unwrap_or(first_entry).as_ref() }, elapsed_ns, ); } } - if let Some(entry_ptr) = sequence.test_entry { + if let Some(entry_ptr) = test_entry { // SAFETY: arena-owned entry let entry = unsafe { entry_ptr.as_ref() }; if entry.base.test_id_for_debugger != 0 { @@ -697,7 +691,7 @@ impl Execution { use bun_jsc::Debugger::TestStatus as S; debugger.test_reporter_agent.report_test_end( entry.base.test_id_for_debugger, - match sequence.result { + match result { Result::Pass => S::Pass, Result::Fail => S::Fail, Result::Skip => S::Skip, @@ -774,14 +768,12 @@ impl Execution { ) -> HandleUncaughtExceptionResult { let _g = group_begin!(); - let Some((sequence_ptr, _group_ptr)) = + let Some((seq_abs, _group_index)) = self.get_current_and_valid_execution_sequence(user_data) else { return HandleUncaughtExceptionResult::ShowUnhandledErrorBetweenTests; }; - // SAFETY: sequence_ptr points into self.sequences; `self` is not accessed for the - // remainder of this function, so this is the unique live `&mut` to that element. - let sequence = unsafe { &mut *sequence_ptr.as_ptr() }; + let sequence = &mut self.sequences[seq_abs]; sequence.maybe_skip = true; if sequence.active_entry != sequence.test_entry { @@ -821,28 +813,24 @@ pub(crate) fn step_group( now: &mut Timespec, ) -> JsResult { let _g = group_begin!(); - let buntest = buntest_strong.get(); - let this = &mut buntest.execution; loop { - // Carry the active group as NonNull so it does not alias `&mut Execution` re-derived - // inside step_group_one. - let group_ptr: NonNull = match this.active_group() { - Some(g) => NonNull::from(g), - None => return Ok(StepResult::Complete), - }; - { - // SAFETY: group_ptr points into this.groups; only this scope holds a `&mut` to it. - let group = unsafe { &mut *group_ptr.as_ptr() }; - if !group.executing { - Execution::on_group_started(global_this); - group.executing = true; + // Carry the active group as an index; the element borrow is re-derived at each use. + let group_index = { + let this = &buntest_strong.get().execution; + if this.group_index >= this.groups.len() { + return Ok(StepResult::Complete); } + this.group_index + }; + if !buntest_strong.get().execution.groups[group_index].executing { + Execution::on_group_started(global_this); + buntest_strong.get().execution.groups[group_index].executing = true; } // loop over items in the group and advance their execution - let status = step_group_one(buntest_strong, global_this, group_ptr, now)?; + let status = step_group_one(buntest_strong, global_this, group_index, now)?; match status { AdvanceStatus::Execute { timeout } => { return Ok(StepResult::Waiting { timeout }); @@ -850,14 +838,15 @@ pub(crate) fn step_group( AdvanceStatus::Done => {} } - // SAFETY: re-deref after step_group_one; disjoint from this.sequences read below. - let group = unsafe { &mut *group_ptr.as_ptr() }; - group.executing = false; + buntest_strong.get().execution.groups[group_index].executing = false; Execution::on_group_completed(global_this); // if there is one sequence and it failed, skip to the next group - let (start, end, failure_skip_to) = - (group.sequence_start, group.sequence_end, group.failure_skip_to); + let this = &mut buntest_strong.get().execution; + let (start, end, failure_skip_to) = { + let group = &this.groups[group_index]; + (group.sequence_start, group.sequence_end, group.failure_skip_to) + }; let all_failed = 'blk: { for sequence in this.sequences[start..end].iter() { if !sequence.result.is_fail() { @@ -887,7 +876,7 @@ enum AdvanceStatus { fn step_group_one( buntest_strong: &BunTestPtr, global_this: &JSGlobalObject, - group: NonNull, + group_index: usize, now: &mut Timespec, ) -> JsResult { let buntest = buntest_strong.get(); @@ -901,13 +890,12 @@ fn step_group_one( }; let mut active_count: usize = 0; let len = { - // SAFETY: group points into buntest.execution.groups; read-only here. - let g = unsafe { group.as_ref() }; + let g = &buntest.execution.groups[group_index]; g.sequence_end - g.sequence_start }; for sequence_index in 0..len { let sequence_status = - step_sequence(buntest_strong, global_this, group, sequence_index, now)?; + step_sequence(buntest_strong, global_this, group_index, sequence_index, now)?; match sequence_status { AdvanceSequenceStatus::Done => {} AdvanceSequenceStatus::Execute { timeout } => { @@ -939,13 +927,13 @@ enum AdvanceSequenceStatus { fn step_sequence( buntest_strong: &BunTestPtr, global_this: &JSGlobalObject, - group: NonNull, + group_index: usize, sequence_index: usize, now: &mut Timespec, ) -> JsResult { loop { if let Some(r) = - step_sequence_one(buntest_strong, global_this, group, sequence_index, now)? + step_sequence_one(buntest_strong, global_this, group_index, sequence_index, now)? { return Ok(r); } @@ -956,23 +944,17 @@ fn step_sequence( fn step_sequence_one( buntest_strong: &BunTestPtr, global_this: &JSGlobalObject, - group: NonNull, + group_index: usize, sequence_index: usize, now: &mut Timespec, ) -> JsResult> { let _g = group_begin!(); - let buntest = buntest_strong.get(); - let buntest_ptr = NonNull::from(&mut *buntest); - let this = &mut buntest.execution; - - // Locate the sequence by absolute index, then carry it as NonNull so it can coexist with - // `group` (disjoint field) and with later re-borrows through `buntest_ptr` in advance_sequence. - // SAFETY: group points into this.groups; read-only. - let seq_abs = unsafe { group.as_ref() }.sequence_start + sequence_index; - let sequence_ptr: NonNull = NonNull::from(&mut this.sequences[seq_abs]); - // SAFETY: sequence_ptr points into this.sequences; this is the unique live `&mut` to that - // element until we hand it off to advance_sequence (which takes the NonNull, not the &mut). - let sequence = unsafe { &mut *sequence_ptr.as_ptr() }; + let this = &mut buntest_strong.get().execution; + + // Locate the sequence by absolute index; every element borrow is re-derived from + // `buntest_strong` after any call that can re-enter from JS. + let seq_abs = this.groups[group_index].sequence_start + sequence_index; + let sequence = &mut this.sequences[seq_abs]; if sequence.executing { let Some(active_entry_ptr) = sequence.active_entry else { debug_assert!(false); // sequence is executing with no active entry @@ -980,10 +962,10 @@ fn step_sequence_one( timeout: Timespec::EPOCH, })); }; - // SAFETY: arena-owned entry - let active_entry = unsafe { &mut *active_entry_ptr.as_ptr() }; + // SAFETY: arena-owned entry; `evaluate_timeout` only needs `&self`. + let active_entry = unsafe { active_entry_ptr.as_ref() }; if active_entry.evaluate_timeout(sequence, now) { - Execution::advance_sequence(buntest_ptr, sequence_ptr, group); + Execution::advance_sequence(buntest_strong, seq_abs, group_index); return Ok(None); // run again } group_log::log(format_args!("runOne: can't advance; already executing")); @@ -999,13 +981,14 @@ fn step_sequence_one( group_log::log(format_args!("runOne: no more entries; sequence complete.")); return Ok(Some(AdvanceSequenceStatus::Done)); }; - // SAFETY: arena-owned entry - let next_item = unsafe { &mut *next_item_ptr.as_ptr() }; sequence.executing = true; if Some(next_item_ptr) == sequence.first_entry { Execution::on_sequence_started(sequence); } - Execution::on_entry_started(next_item); + // SAFETY: arena-owned entry; the `&mut` dies with the call. + Execution::on_entry_started(unsafe { &mut *next_item_ptr.as_ptr() }); + // SAFETY: arena-owned entry; shared read, dead before user JS runs below. + let next_item = unsafe { next_item_ptr.as_ref() }; if let Some(cb) = next_item.callback.as_ref() { group_log::log(format_args!("runSequence queued callback")); @@ -1028,28 +1011,33 @@ fn step_sequence_one( (*on_stack_cell).set(prev_on_stack); }); + // Copy out what the callback needs: `run_test_callback` runs user JS, which re-derives + // `&mut BunTest`, so no borrow of the entry or of `execution` may span it. + let callback = cb.get(); + let has_done_parameter = next_item.has_done_parameter; + let entry_timespec = next_item.timespec; + if BunTest::run_test_callback( buntest_strong, global_this, - cb.get(), - next_item.has_done_parameter, + callback, + has_done_parameter, callback_data, - &next_item.timespec, + &entry_timespec, ) .is_some() { *now = Timespec::now_force_real_time(); - // SAFETY: re-deref after run_test_callback; sequence_ptr still valid (sequences is a - // Box<[ExecutionSequence]>, never reallocated during execution). - let sequence = unsafe { &mut *sequence_ptr.as_ptr() }; - let _ = next_item.evaluate_timeout(sequence, now); + let sequence = &mut buntest_strong.get().execution.sequences[seq_abs]; + // SAFETY: arena-owned entry; re-derived after user JS ran. + let _ = unsafe { next_item_ptr.as_ref() }.evaluate_timeout(sequence, now); // the result is available immediately; advance the sequence and run again. - Execution::advance_sequence(buntest_ptr, sequence_ptr, group); + Execution::advance_sequence(buntest_strong, seq_abs, group_index); return Ok(None); // run again } return Ok(Some(AdvanceSequenceStatus::Execute { - timeout: next_item.timespec, + timeout: entry_timespec, })); } else { match next_item.base.mode { @@ -1079,7 +1067,7 @@ fn step_sequence_one( debug_assert!(false); } } - Execution::advance_sequence(buntest_ptr, sequence_ptr, group); + Execution::advance_sequence(buntest_strong, seq_abs, group_index); return Ok(None); // run again } } diff --git a/src/runtime/test_runner/Order.rs b/src/runtime/test_runner/Order.rs index 00f6a69fd336..cacb15872f7b 100644 --- a/src/runtime/test_runner/Order.rs +++ b/src/runtime/test_runner/Order.rs @@ -82,7 +82,7 @@ impl Order { if current.failed { return Ok(()); // do not schedule any tests in a failed describe scope } - let use_hooks = self.cfg.always_use_hooks || current.base.has_callback; + let use_hooks = self.cfg.always_use_hooks || current.base.has_callback.get(); // gather beforeAll let beforeall_order: AllOrderResult = if use_hooks { @@ -98,9 +98,9 @@ impl Order { // gather children // reshaped for borrowck — iterate by index since generate_order_sub borrows &mut self. - let scope_only = current.base.only; + let scope_only = current.base.only.get(); for i in 0..current.entries.len() { - if scope_only == Only::Contains && current.entries[i].base().only == Only::No { + if scope_only == Only::Contains && current.entries[i].base().only.get() == Only::No { continue; } self.generate_order_sub(&mut current.entries[i])?; @@ -131,20 +131,19 @@ impl Order { // loop below, so we never hold a long-lived `&mut` to it across those calls — each access // dereferences the pointer locally. // SAFETY: caller-guaranteed live `ExecutionEntry` (see safety doc above); read-only field access. - debug_assert!(unsafe { current.as_ref().base.has_callback == current.as_ref().callback.is_some() }); + debug_assert!(unsafe { current.as_ref().base.has_callback.get() == current.as_ref().callback.is_some() }); // SAFETY: caller-guaranteed live `ExecutionEntry` (see above); read-only field access. - let use_each_hooks = unsafe { current.as_ref().base.has_callback }; + let use_each_hooks = unsafe { current.as_ref().base.has_callback.get() }; // SAFETY: caller-guaranteed live `ExecutionEntry` (see above); read-only field access. - let first_parent: Option<*mut DescribeScope> = unsafe { current.as_ref().base.parent }; + let first_parent = unsafe { current.as_ref().base.parent }; let mut list = EntryList::default(); // gather beforeEach (alternatively, this could be implemented recursively to make it less complicated) if use_each_hooks { - let mut parent: Option<*mut DescribeScope> = first_parent; - while let Some(p_ptr) = parent { - // SAFETY: parent chain consists of live DescribeScope nodes. - let p = unsafe { &*p_ptr }; + let mut parent = first_parent; + while let Some(p_ref) = parent { + let p = p_ref.get(); // prepend in reverse so they end up in forwards order let mut i: usize = p.before_each.len(); while i > 0 { @@ -168,10 +167,9 @@ impl Order { // gather afterEach if use_each_hooks { - let mut parent: Option<*mut DescribeScope> = first_parent; - while let Some(p_ptr) = parent { - // SAFETY: parent chain consists of live DescribeScope nodes. - let p = unsafe { &*p_ptr }; + let mut parent = first_parent; + while let Some(p_ref) = parent { + let p = p_ref.get(); for entry in p.after_each.iter() { let src: *const ExecutionEntry = &raw const **entry; // SAFETY: `src` is valid for reads; `Drop` never runs on the bitwise copy diff --git a/src/runtime/test_runner/ScopeFunctions.rs b/src/runtime/test_runner/ScopeFunctions.rs index 52a454333cba..c7aba07555d6 100644 --- a/src/runtime/test_runner/ScopeFunctions.rs +++ b/src/runtime/test_runner/ScopeFunctions.rs @@ -2,7 +2,7 @@ use core::fmt; use crate::test_runner::expect::JSValueTestExt; use core::sync::atomic::{AtomicI32, Ordering}; -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsClass, JsResult}; +use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsClass, JsResult, RegularExpression}; use bun_core::String as BunString; use crate::test_runner::bun_test::{self, BaseScopeCfg, BunTest, DescribeScope}; @@ -304,10 +304,7 @@ fn filter_names(rem: &mut R, description: Option<&[u8]>, parent_in: rem.write_end(description.unwrap_or(b"")); let mut parent = parent_in; while let Some(scope) = parent { - // PORTING.md: `BaseScope.parent` is `Option<*const DescribeScope>` (raw backref); - // per-use reborrow. - // SAFETY: parent backrefs are stable for the lifetime of the collection tree. - parent = scope.base.parent.map(|p| unsafe { &*p }); + parent = scope.base.parent.as_ref().map(|p| p.get()); if scope.base.name.is_none() { continue; } @@ -426,10 +423,9 @@ impl ScopeFunctions { "matches_filter \"{}\"", bstr::BStr::new(bun_test.collection.filter_buffer.as_slice()) )); - // SAFETY: `filter_regex` is the FFI-allocated Yarr handle stored in - // `TestRunner` for the process lifetime; single-threaded here so the - // exclusive borrow is unaliased. - matches_filter = unsafe { &mut *filter_regex.as_ptr() }.matches(str); + // `RegularExpression` is an `opaque_ffi!` ZST handle; `opaque_mut` is + // the centralised non-null deref proof. + matches_filter = RegularExpression::opaque_mut(filter_regex.as_ptr()).matches(str); bun_test.collection.filter_buffer.clear(); } diff --git a/src/runtime/test_runner/bun_test.rs b/src/runtime/test_runner/bun_test.rs index a59d41f484d2..ad2e78b930ff 100644 --- a/src/runtime/test_runner/bun_test.rs +++ b/src/runtime/test_runner/bun_test.rs @@ -1,11 +1,12 @@ use core::fmt; use core::ptr::NonNull; -use std::cell::UnsafeCell; +use std::cell::Cell; use std::rc::{Rc, Weak}; use bun_collections::LinearFifo; use bun_core::{Output, Timespec}; -use bun_jsc::{self as jsc, CallFrame, GlobalRef, JSGlobalObject, JSValue, JsResult, Strong, JsClass as _}; +use bun_jsc::{self as jsc, CallFrame, GlobalRef, JSGlobalObject, JSValue, JsCell, JsResult, Strong, JsClass as _}; +use bun_ptr::ParentRef; use bun_jsc::virtual_machine::VirtualMachine; use bun_jsc::js_promise::Status as PromiseStatus; use super::jest::{Jest, FileId, FileColumns as _}; @@ -235,7 +236,7 @@ pub mod js_fns { } Phase::Execution => { let active = bun_test.get_current_state_data(); - let Some((sequence, _)) = bun_test.execution.get_current_and_valid_execution_sequence(&active) else { + let Some((seq_abs, _)) = bun_test.execution.get_current_and_valid_execution_sequence(&active) else { return Err(if tag == GenericHookTag::OnTestFinished { global_this.throw(format_args!( "Cannot call {}() here. It cannot be called inside a concurrent test. Use test.serial or remove test.concurrent.", @@ -249,9 +250,7 @@ pub mod js_fns { }); }; - // SAFETY: `get_current_and_valid_execution_sequence` returns a NonNull - // into `execution.sequences`; deref at point-of-use only. - let sequence_ref = unsafe { sequence.as_ref() }; + let sequence_ref = &bun_test.execution.sequences[seq_abs]; let append_point: *mut ExecutionEntry = match tag { GenericHookTag::AfterAll | GenericHookTag::AfterEach => 'blk: { let mut iter = sequence_ref.active_entry; @@ -362,21 +361,21 @@ pub type BunTestPtr = Rc; pub type BunTestPtrWeak = Weak; pub type BunTestPtrOptional = Option>; -/// `UnsafeCell` newtype so `Rc` permits mutation of the shared -/// `BunTest` (`UnsafeCell` is required for any write reachable through a -/// shared/`*const` path). +/// `JsCell` newtype so `Rc` permits mutation of the shared +/// `BunTest` (interior mutability is required for any write reachable through +/// a shared/`*const` path). #[repr(transparent)] -pub struct BunTestCell(UnsafeCell); +pub struct BunTestCell(JsCell); impl BunTestCell { #[inline] pub fn new(bt: BunTest) -> Rc { - Rc::new(Self(UnsafeCell::new(bt))) + Rc::new(Self(JsCell::new(bt))) } /// Returns `&mut` because every call site mutates. The borrow is derived - /// from `UnsafeCell::get()` so provenance is valid for writes even while - /// other `Rc`/`Weak` handles exist. + /// from `JsCell` so provenance is valid for writes even while other + /// `Rc`/`Weak` handles exist. /// /// **Aliasing contract:** the test runner is single-threaded. Callers must not /// hold the returned `&mut` across a re-entrancy point (JS callback, @@ -386,16 +385,16 @@ impl BunTestCell { #[inline] #[allow(clippy::mut_from_ref)] pub fn get(&self) -> &mut BunTest { - // SAFETY: `UnsafeCell` interior; single-threaded JS VM. See contract above. - unsafe { &mut *self.0.get() } + // SAFETY: single-threaded JS VM; callers re-derive across re-entrancy + // points rather than holding this borrow. See contract above. + unsafe { self.0.get_mut() } } /// Raw pointer for sites that must span re-entrant `.get()` calls without - /// holding a live `&mut` (Stacked-Borrows-safe: raw ptrs do not assert - /// uniqueness). + /// holding a live `&mut` (raw ptrs do not assert uniqueness). #[inline] pub fn as_ptr(&self) -> *mut BunTest { - self.0.get() + self.0.as_ptr() } } @@ -403,20 +402,20 @@ impl core::ops::Deref for BunTestCell { type Target = BunTest; #[inline] fn deref(&self) -> &BunTest { - // SAFETY: shared read through `UnsafeCell`; single-threaded — caller - // must not hold a live `&mut` from `.get()` concurrently. - unsafe { &*self.0.get() } + // Shared read; single-threaded — caller must not hold a live `&mut` + // from `.get()` concurrently. + self.0.get() } } /// Back-compat shim for sibling modules (jest.rs) that funneled through this -/// helper. Now routes through `UnsafeCell::get()` instead of the UB +/// helper. Now routes through `JsCell` instead of the UB /// `*const T as *mut T` cast. /// /// # Safety /// Caller must uphold the aliasing contract documented on [`BunTestCell::get`]. #[inline] -#[allow(clippy::mut_from_ref)] // interior mutability: routes through UnsafeCell::get() +#[allow(clippy::mut_from_ref)] // interior mutability: routes through JsCell pub unsafe fn buntest_as_mut(ptr: &BunTestPtr) -> &mut BunTest { ptr.get() } @@ -438,8 +437,8 @@ impl BunTestRoot { name: None, concurrent: false, mode: ScopeMode::Normal, - only: Only::No, - has_callback: false, + only: Cell::new(Only::No), + has_callback: Cell::new(false), test_id_for_debugger: 0, line_no: 0, }); @@ -460,8 +459,8 @@ impl BunTestRoot { name: None, concurrent: false, mode: ScopeMode::Normal, - only: Only::No, - has_callback: false, + only: Cell::new(Only::No), + has_callback: Cell::new(false), test_id_for_debugger: 0, line_no: 0, }); @@ -1060,7 +1059,7 @@ impl BunTest { let should_randomize = per_file_prng.take(); let mut order = Order::Order::init(Order::Config { - always_use_hooks: self.collection.root_scope.base.only == Only::No && !has_filter, + always_use_hooks: self.collection.root_scope.base.only.get() == Only::No && !has_filter, randomize: should_randomize, }); @@ -1445,11 +1444,10 @@ impl RefDataValue { if buntest.phase != Phase::Execution { return None; } - let (the_sequence, _) = buntest.execution.get_current_and_valid_execution_sequence(self)?; - // SAFETY: `the_sequence` is a NonNull into `execution.sequences`; deref - // at point-of-use only. `active_entry` is a valid intrusive node while - // the sequence is live. - unsafe { the_sequence.as_ref().active_entry.map(|p| &mut *p.as_ptr()) } + let (seq_abs, _) = buntest.execution.get_current_and_valid_execution_sequence(self)?; + let active_entry = buntest.execution.sequences[seq_abs].active_entry?; + // SAFETY: `active_entry` is a valid intrusive node while the sequence is live. + unsafe { Some(&mut *active_entry.as_ptr()) } } } @@ -1664,12 +1662,12 @@ impl Only { } pub struct BaseScope { - pub parent: Option<*mut DescribeScope>, + pub parent: Option>, pub name: Option>, pub concurrent: bool, pub mode: ScopeMode, - pub only: Only, - pub has_callback: bool, + pub only: Cell, + pub has_callback: Cell, /// this value is 0 unless the debugger is active and the scope has a debugger id pub test_id_for_debugger: i32, /// only available if using junit reporter, otherwise 0 @@ -1679,12 +1677,10 @@ impl BaseScope { pub fn init( cfg: BaseScopeCfg, name_not_owned: Option<&[u8]>, - parent: Option<*mut DescribeScope>, + parent: Option>, has_callback: bool, ) -> BaseScope { - // SAFETY: `parent` is a live backref into the describe tree; single-threaded, - // and the parent outlives this child during construction. - let parent_base = parent.map(|p| unsafe { &(*p).base }); + let parent_base = parent.as_ref().map(|p| &p.get().base); BaseScope { parent, name: name_not_owned.map(Box::<[u8]>::from), @@ -1698,24 +1694,21 @@ impl BaseScope { } else { cfg.self_mode }, - only: if cfg.self_only { Only::Yes } else { Only::No }, - has_callback, + only: Cell::new(if cfg.self_only { Only::Yes } else { Only::No }), + has_callback: Cell::new(has_callback), test_id_for_debugger: cfg.test_id_for_debugger, line_no: cfg.line_no, } } pub fn propagate(&mut self, has_callback: bool) { - self.has_callback = has_callback; + self.has_callback.set(has_callback); if let Some(parent) = self.parent { - // SAFETY: parent backref valid; tree is single-threaded and parent - // outlives child. - let parent = unsafe { &mut *parent }; - if self.only != Only::No { - parent.mark_contains_only(); + if self.only.get() != Only::No { + parent.get().mark_contains_only(); } - if self.has_callback { - parent.mark_has_callback(); + if self.has_callback.get() { + parent.get().mark_has_callback(); } } } @@ -1755,30 +1748,28 @@ impl DescribeScope { } // destroy → Drop on Box; all fields own their contents. - fn mark_contains_only(&mut self) { - let mut target: Option<*mut DescribeScope> = Some(std::ptr::from_mut(self)); - while let Some(scope_ptr) = target { - // SAFETY: walking parent backrefs; tree is single-threaded - let scope = unsafe { &mut *scope_ptr }; - if scope.base.only == Only::Contains { + fn mark_contains_only(&self) { + let mut scope: &DescribeScope = self; + loop { + if scope.base.only.get() == Only::Contains { return; // already marked } // note that we overwrite '.yes' with '.contains' to support only-inside-only - scope.base.only = Only::Contains; - target = scope.base.parent; + scope.base.only.set(Only::Contains); + let Some(parent) = scope.base.parent.as_ref() else { return }; + scope = parent.get(); } } - fn mark_has_callback(&mut self) { - let mut target: Option<*mut DescribeScope> = Some(std::ptr::from_mut(self)); - while let Some(scope_ptr) = target { - // SAFETY: walking parent backrefs; tree is single-threaded - let scope = unsafe { &mut *scope_ptr }; - if scope.base.has_callback { + fn mark_has_callback(&self) { + let mut scope: &DescribeScope = self; + loop { + if scope.base.has_callback.get() { return; // already marked } - scope.base.has_callback = true; - target = scope.base.parent; + scope.base.has_callback.set(true); + let Some(parent) = scope.base.parent.as_ref() else { return }; + scope = parent.get(); } } @@ -1788,7 +1779,9 @@ impl DescribeScope { name_not_owned: Option<&[u8]>, base: BaseScopeCfg, ) -> &mut DescribeScope { - let mut child = Self::create(BaseScope::init(base, name_not_owned, Some(std::ptr::from_mut(self)), false)); + // SAFETY: `self` outlives the child it is about to own; `from_mut` keeps write provenance. + let parent = unsafe { ParentRef::from_raw_mut(std::ptr::from_mut(self)) }; + let mut child = Self::create(BaseScope::init(base, name_not_owned, Some(parent), false)); child.base.propagate(false); self.entries.push(TestScheduleEntry::Describe(child)); match self.entries.last_mut().unwrap() { @@ -1805,7 +1798,9 @@ impl DescribeScope { base: BaseScopeCfg, phase: AddedInPhase, ) -> JsResult<&mut ExecutionEntry> { - let mut entry = ExecutionEntry::create(name_not_owned, callback, cfg, Some(std::ptr::from_mut(self)), base, phase); + // SAFETY: `self` outlives the entry it is about to own; `from_mut` keeps write provenance. + let parent = unsafe { ParentRef::from_raw_mut(std::ptr::from_mut(self)) }; + let mut entry = ExecutionEntry::create(name_not_owned, callback, cfg, Some(parent), base, phase); let has_cb = entry.callback.is_some(); entry.base.propagate(has_cb); self.entries.push(TestScheduleEntry::TestCallback(entry)); @@ -1834,7 +1829,9 @@ impl DescribeScope { base: BaseScopeCfg, phase: AddedInPhase, ) -> JsResult<&mut ExecutionEntry> { - let entry = ExecutionEntry::create(None, callback, cfg, Some(std::ptr::from_mut(self)), base, phase); + // SAFETY: `self` outlives the hook it is about to own; `from_mut` keeps write provenance. + let parent = unsafe { ParentRef::from_raw_mut(std::ptr::from_mut(self)) }; + let entry = ExecutionEntry::create(None, callback, cfg, Some(parent), base, phase); let list = self.get_hook_entries(tag); list.push(entry); Ok(&mut **list.last_mut().unwrap()) @@ -1892,7 +1889,7 @@ impl ExecutionEntry { name_not_owned: Option<&[u8]>, cb: Option, cfg: ExecutionEntryCfg, - parent: Option<*mut DescribeScope>, + parent: Option>, base: BaseScopeCfg, phase: AddedInPhase, ) -> Box { diff --git a/src/runtime/test_runner/debug.rs b/src/runtime/test_runner/debug.rs index 74486c9399dd..1cf3f1829b93 100644 --- a/src/runtime/test_runner/debug.rs +++ b/src/runtime/test_runner/debug.rs @@ -29,8 +29,8 @@ pub(crate) fn dump_describe(describe: &DescribeScope) -> JsResult<()> { bstr::BStr::new(describe.base.name.as_deref().unwrap_or(b"(unnamed)")), describe.base.concurrent, describe.base.mode.tag_name(), - describe.base.only.tag_name(), - describe.base.has_callback, + describe.base.only.get().tag_name(), + describe.base.has_callback.get(), )); for entry in describe.before_all.as_slice() { @@ -60,7 +60,7 @@ pub(crate) fn dump_test(current: &ExecutionEntry, label: &[u8]) -> JsResult<()> bstr::BStr::new(label), bstr::BStr::new(current.base.name.as_deref().unwrap_or(b"(unnamed)")), current.base.concurrent, - current.base.only.tag_name(), + current.base.only.get().tag_name(), )); Ok(()) } @@ -93,8 +93,8 @@ pub(crate) fn dump_order(this: &Execution) -> JsResult<()> { bstr::BStr::new(entry.base.name.as_deref().unwrap_or(b"(unnamed)")), entry.base.concurrent, entry.base.mode.tag_name(), - entry.base.only.tag_name(), - entry.base.has_callback, + entry.base.only.get().tag_name(), + entry.base.has_callback.get(), )); current_entry = entry.next.and_then(NonNull::new); } diff --git a/src/runtime/test_runner/expect.rs b/src/runtime/test_runner/expect.rs index bdb88a682c81..316fc93a8776 100644 --- a/src/runtime/test_runner/expect.rs +++ b/src/runtime/test_runner/expect.rs @@ -560,8 +560,7 @@ impl Expect { let mut length: usize = 0; let mut curr_scope = execution_entry.base.parent; while let Some(scope) = curr_scope { - // SAFETY: `parent` is a live `*mut DescribeScope` owned by the BunTest arena. - let scope = unsafe { &*scope }; + let scope = scope.get(); if let Some(name) = scope.base.name.as_deref() { if !name.is_empty() { length += name.len() + 1; @@ -590,8 +589,7 @@ impl Expect { // copy describe scopes in reverse order curr_scope = execution_entry.base.parent; while let Some(scope) = curr_scope { - // SAFETY: `parent` is a live `*mut DescribeScope` owned by the BunTest arena. - let scope = unsafe { &*scope }; + let scope = scope.get(); if let Some(name) = scope.base.name.as_deref() { if !name.is_empty() { index -= name.len() + 1; @@ -827,14 +825,16 @@ impl Expect { return Err(global_this.throw(format_args!("Expected value must be a function"))); } - let mut return_value: JSValue = JSValue::ZERO; + // `Cell` so the slot handed to the VM stays writable while this frame + // keeps reading it; no `&mut` ever spans `value.call` below. + let return_value: Cell = Cell::new(JSValue::ZERO); // Drain existing unhandled rejections vm.global().handle_rejected_promises(); let scope = vm.unhandled_rejection_scope(); let prev_unhandled_pending_rejection_to_capture = vm.unhandled_pending_rejection_to_capture; - vm.unhandled_pending_rejection_to_capture = Some(&raw mut return_value); + vm.unhandled_pending_rejection_to_capture = Some(core::ptr::NonNull::from(&return_value)); vm.on_unhandled_rejection = VirtualMachine::on_quiet_unhandled_rejection_handler_capture_value; return_value_from_function = match value.call(global_this, JSValue::UNDEFINED, &[]) { Ok(v) => v, @@ -844,11 +844,11 @@ impl Expect { vm.global().handle_rejected_promises(); - if return_value.is_empty() { - return_value = return_value_from_function; + if return_value.get().is_empty() { + return_value.set(return_value_from_function); } - if let Some(promise) = return_value.as_any_promise() { + if let Some(promise) = return_value.get().as_any_promise() { vm.wait_for_promise(promise); scope.apply(vm); match promise.unwrap(global_this.vm(), js_promise::UnwrapMode::MarkHandled) { @@ -863,7 +863,7 @@ impl Expect { } } - if return_value != return_value_from_function { + if return_value.get() != return_value_from_function { if let Some(existing) = return_value_from_function.as_any_promise() { existing.set_handled(global_this.vm()); } @@ -872,7 +872,7 @@ impl Expect { scope.apply(vm); Ok(( - return_value.to_error().or_else(|| return_value_from_function.to_error()), + return_value.get().to_error().or_else(|| return_value_from_function.to_error()), return_value_from_function, )) } diff --git a/src/runtime/test_runner/pretty_format.rs b/src/runtime/test_runner/pretty_format.rs index 7eb035eefde7..dadfad48338b 100644 --- a/src/runtime/test_runner/pretty_format.rs +++ b/src/runtime/test_runner/pretty_format.rs @@ -1613,11 +1613,7 @@ impl<'a> Formatter<'a> { // bodies re-enter this formatter through the `ConsoleFormatter` impl // below for nested values, so the byte sink is wrapped in `AsFmt` (a // `core::fmt::Write` view of the same writer). - if let Some(response) = value.as_::() { - // SAFETY: `as_` returned non-null; the GC keeps the cell alive while - // `value` is on the stack (conservative scan). `write_format` does not - // re-enter `as_` for the same cell, so the `&mut` is unique here. - let response = unsafe { &mut *response }; + if let Some(response) = value.as_class_ref::() { let mut bridge = AsFmt::new(&mut *writer.ctx); if response .write_format::<_, _, ENABLE_ANSI_COLORS>(self, &mut bridge) @@ -1633,9 +1629,7 @@ impl<'a> Formatter<'a> { } return Err(JsError::Thrown); } - } else if let Some(request) = value.as_::() { - // SAFETY: see Response branch above. - let request = unsafe { &mut *request }; + } else if let Some(request) = value.as_class_ref::() { let mut bridge = AsFmt::new(&mut *writer.ctx); if request .write_format::<_, _, ENABLE_ANSI_COLORS>(value, self, &mut bridge) @@ -1652,10 +1646,7 @@ impl<'a> Formatter<'a> { return Err(JsError::Thrown); } return Ok(()); - } else if let Some(build) = value.as_::() { - // SAFETY: see Response branch above. `write_format` is - // `&self` post-R-2, so a shared borrow is sufficient. - let build = unsafe { &*build }; + } else if let Some(build) = value.as_class_ref::() { let mut bridge = AsFmt::new(&mut *writer.ctx); if build .write_format::<_, _, ENABLE_ANSI_COLORS>(self, &mut bridge) @@ -1671,9 +1662,7 @@ impl<'a> Formatter<'a> { } return Err(JsError::Thrown); } - } else if let Some(blob) = value.as_::() { - // SAFETY: see Response branch above. - let blob = unsafe { &mut *blob }; + } else if let Some(blob) = value.as_class_ref::() { let mut bridge = AsFmt::new(&mut *writer.ctx); if blob .write_format::<_, _, ENABLE_ANSI_COLORS>(self, &mut bridge) diff --git a/src/runtime/test_runner/snapshot.rs b/src/runtime/test_runner/snapshot.rs index e107342b0281..a7f547bc53bb 100644 --- a/src/runtime/test_runner/snapshot.rs +++ b/src/runtime/test_runner/snapshot.rs @@ -522,24 +522,11 @@ impl<'a> Snapshots<'a> { } next_start += fn_name.len(); - // `Lexer.initWithoutReading` and `TSXParser.init` both need the same - // `Log`, but Rust forbids two live `&'a mut Log`; - // derive a raw pointer so borrowck doesn't track the lexer/parser borrow, - // matching the pattern in `js_parser::Parser::init`. The unique `&mut` - // logically lives inside `parser.lexer`; `log.add_error_fmt` calls below - // reborrow via the scopeguard between parser uses. - // SAFETY: `log` outlives the `'blk` block; lexer/parser are dropped at - // block exit (or `continue 'ils`). See Parser.rs:214 for the provenance - // discussion. - let log_ptr: *mut bun_ast::Log = &raw mut *log; - let mut lexer = js_lexer::Lexer::init_without_reading( - // SAFETY: `log_ptr` derived from `&raw mut *log` just above; `log` - // outlives `'blk` and no other `&mut Log` is live until `lexer` is - // moved into `parser` below. - unsafe { &mut *log_ptr }, - &source, - &arena, - ); + // `init_without_reading` borrows `log` only for the duration of the + // call (it stores a `NonNull`), so the reborrow ends here and the + // `log.add_error_fmt` calls below can reborrow the scopeguard again. + let mut lexer = + js_lexer::Lexer::init_without_reading(&mut *log, &source, &arena); if next_start > 0 { // equivalent to lexer.consumeRemainderBytes(next_start) lexer.current += next_start - (lexer.current - lexer.end); @@ -561,10 +548,14 @@ impl<'a> Snapshots<'a> { // `P::init` writes a fully-initialized value on `Ok`. On `Err` we // `?`-return before arming the drop guard, so the slot stays // uninitialized and untouched. + // Copy the lexer's `NonNull` so `P.log` and `Lexer.log` share one + // provenance chain and neither aliases the other foreignly, exactly as + // `js_parser::Parser::init` does. + let log_ptr = lexer.log; js_parser::TSXParser::init( &mut __parser_slot, &arena, - core::ptr::NonNull::new(log_ptr).expect("log_ptr derived from &mut *log"), + log_ptr, &source, &vm.transpiler.options.define, lexer, diff --git a/src/runtime/timer/EventLoopDelayMonitor.rs b/src/runtime/timer/EventLoopDelayMonitor.rs index 06173233d08c..e231546943aa 100644 --- a/src/runtime/timer/EventLoopDelayMonitor.rs +++ b/src/runtime/timer/EventLoopDelayMonitor.rs @@ -1,15 +1,14 @@ use bun_jsc::JSValue; use bun_jsc::virtual_machine::VirtualMachine; +use bun_ptr::BackRef; // Export functions for C++ #[unsafe(no_mangle)] pub(super) extern "C" fn Timer_enableEventLoopDelayMonitoring( - vm: *mut VirtualMachine, + vm: BackRef, histogram: JSValue, resolution_ms: i32, ) { - // SAFETY: vm is a valid non-null pointer passed from C++. - let vm = unsafe { &mut *vm }; // `vm.timer` is `()` (jsc/runtime crate cycle) — recover `All` via runtime_state(). let state = crate::jsc_hooks::runtime_state(); // SAFETY: `runtime_state()` is non-null after `bun_runtime::init()`; single @@ -23,9 +22,7 @@ pub(super) extern "C" fn Timer_enableEventLoopDelayMonitoring( } #[unsafe(no_mangle)] -pub(super) extern "C" fn Timer_disableEventLoopDelayMonitoring(vm: *mut VirtualMachine) { - // SAFETY: vm is a valid non-null pointer passed from C++. - let vm = unsafe { &mut *vm }; +pub(super) extern "C" fn Timer_disableEventLoopDelayMonitoring(vm: BackRef) { let state = crate::jsc_hooks::runtime_state(); // SAFETY: see `Timer_enableEventLoopDelayMonitoring`. unsafe { (*state).timer.event_loop_delay.disable(vm) }; diff --git a/src/runtime/timer/mod.rs b/src/runtime/timer/mod.rs index 9e0aab5c4459..526e6c5a9f93 100644 --- a/src/runtime/timer/mod.rs +++ b/src/runtime/timer/mod.rs @@ -78,7 +78,7 @@ macro_rules! impl_timer_object { unsafe fn destructor(this: *mut Self, _ctx: ()) { // SAFETY: `raw_count == 0` ⇒ unique ownership; `deinit` // consumes the `heap::alloc`'d allocation from `init_with()`. - unsafe { Self::deinit(this) } + unsafe { Self::deinit(::bun_core::heap::take(this)) } } } @@ -169,17 +169,16 @@ macro_rules! impl_timer_object { } /// Called via `RefCounted::destructor` when the refcount reaches - /// zero. Not `impl Drop`: this fn frees the backing `Box` itself. + /// zero. Takes ownership; the `Box` drops when this returns. /// /// # Safety - /// `this` must be the unique owner (refcount == 0) of a - /// `heap::alloc`'d `Self`. - unsafe fn deinit(this: *mut Self) { - // SAFETY: refcount has reached zero ⇒ unique reference. - unsafe { - (*this).internals.deinit(); - drop(::bun_core::heap::take(this)); - } + /// Refcount must be zero, and `internals.deinit()` requires the + /// per-thread `RuntimeState`/`VirtualMachine` to be installed. + // `boxed_local`: the `Box` is the ownership unit being reclaimed here. + #[allow(clippy::boxed_local)] + unsafe fn deinit(mut self: ::std::boxed::Box) { + // SAFETY: caller contract. + unsafe { self.internals.deinit() }; } // C-ABI shim (`${name}Class__construct`) is emitted by @@ -455,7 +454,7 @@ impl EventLoopDelayMonitor { pub(crate) fn enable( &mut self, - _vm: &mut bun_jsc::virtual_machine::VirtualMachine, + _vm: ::bun_ptr::BackRef, histogram: JSValue, resolution_ms: i32, ) { @@ -479,7 +478,10 @@ impl EventLoopDelayMonitor { unsafe { (*Self::timer_all()).insert(elt) }; } - pub(crate) fn disable(&mut self, _vm: &mut bun_jsc::virtual_machine::VirtualMachine) { + pub(crate) fn disable( + &mut self, + _vm: ::bun_ptr::BackRef, + ) { if !self.enabled { return; } diff --git a/src/runtime/valkey_jsc/js_valkey.rs b/src/runtime/valkey_jsc/js_valkey.rs index 2a5b66ae35e3..770fb8157e37 100644 --- a/src/runtime/valkey_jsc/js_valkey.rs +++ b/src/runtime/valkey_jsc/js_valkey.rs @@ -122,7 +122,7 @@ impl SubscriptionCtx { }) } - fn subscription_callback_map(&self) -> &mut JSMap { + fn subscription_callback_map(&self) -> &JSMap { let parent_this = self .parent() .this_value @@ -130,10 +130,10 @@ impl SubscriptionCtx { .try_get() .expect("unreachable"); let value_js = Js::subscription_callback_map_get_cached(parent_this).unwrap(); - // `JSMap` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. + // `JSMap` is an `opaque_ffi!` ZST — `opaque_ref` is the safe deref. // `from_js` returns a non-null heap cell when the slot was set by // `init()`; single JS thread. - JSMap::opaque_mut(JSMap::from_js(value_js).unwrap().as_ptr()) + JSMap::opaque_ref(JSMap::from_js(value_js).unwrap().as_ptr()) } /// Get the total number of channels that this subscription context is subscribed to. @@ -1601,8 +1601,9 @@ impl JSValkeyClient { debug_assert!(!state.is_null(), "RuntimeState not installed"); // SAFETY: per-thread `RuntimeState`; `ssl_ctx_cache` has a // stable address for the VM's lifetime, JS-thread-only. - let cache = unsafe { &mut (*state).ssl_ctx_cache }; - self._secure.set(cache.get_or_create(custom, &mut err)); + let cache = unsafe { &(*state).ssl_ctx_cache }; + self._secure + .set(cache.with_mut(|c| c.get_or_create(custom, &mut err))); } self._secure.get().is_none() } else { diff --git a/src/runtime/webcore/ArrayBufferSink.rs b/src/runtime/webcore/ArrayBufferSink.rs index 4f0bd90b1a49..621720ab461e 100644 --- a/src/runtime/webcore/ArrayBufferSink.rs +++ b/src/runtime/webcore/ArrayBufferSink.rs @@ -83,13 +83,13 @@ impl ArrayBufferSink { /// # Safety /// `this` must be the m_ctx payload allocated via `heap::alloc` in /// init/JSSink, called from JSC lazy sweep on the mutator thread. - // Forwards `this` to `destroy` without dereferencing it here; - // not_unsafe_ptr_arg_deref is a false positive on this forwarding wrapper. + // not_unsafe_ptr_arg_deref: the `# Safety` contract above stands in for the + // `unsafe fn` this codegen-facing thunk cannot be. #[allow(clippy::not_unsafe_ptr_arg_deref)] pub fn finalize(this: *mut Self) { // SAFETY: `this` is the heap-allocated m_ctx payload (see `# Safety` - // above); it has not been freed yet, so `destroy` may reclaim it. - unsafe { Self::destroy(this) }; + // above); it has not been freed yet, so we may reclaim ownership. + Self::destroy(unsafe { bun_core::heap::take(this) }); } pub fn init( @@ -173,13 +173,9 @@ impl ArrayBufferSink { Ok(()) } - /// # Safety - /// `this` must have been allocated via `heap::alloc` (i.e. by - /// [`ArrayBufferSink::init`] or the JSSink codegen path) and not yet freed. - pub unsafe fn destroy(this: *mut Self) { - // SAFETY: reclaiming ownership drops `bytes` (Vec impls Drop) and - // frees the box. - drop(unsafe { bun_core::heap::take(this) }); + /// Drops the sink: `bytes` (Vec impls Drop) is freed with the box. + pub fn destroy(self: Box) { + drop(self); } pub fn to_js( diff --git a/src/runtime/webcore/BakeResponse.rs b/src/runtime/webcore/BakeResponse.rs index 2595a43e9a9d..3b4051776d5b 100644 --- a/src/runtime/webcore/BakeResponse.rs +++ b/src/runtime/webcore/BakeResponse.rs @@ -1,7 +1,7 @@ use core::ffi::{c_int, c_void}; use crate::webcore::Response; -use crate::webcore::response::{HeadersRef, Init}; +use crate::webcore::response::{FetchHeaders, Init}; use bun_core::String as BunString; use bun_jsc::{CallFrame, HTTPHeaderName, JSGlobalObject, JSValue, JsError, JsResult}; @@ -38,22 +38,15 @@ pub enum SSRKind { } /// Create the JS `BakeResponse` wrapper for `this`. The C++ wrapper **adopts** -/// the `*mut Response` allocation (freed in `BakeResponseClass__finalize`), so -/// callers must hand over a heap pointer they no longer own — typically via -/// `heap::alloc`. -/// -/// # Safety -/// `this` must be a valid heap-allocated `Response` whose ownership is being -/// transferred to the JS GC. After this call the caller must not free or -/// dereference `this`. -pub(crate) unsafe fn to_js_for_ssr( - this: *mut Response, +/// the allocation (freed in `BakeResponseClass__finalize`), so ownership of the +/// `Box` transfers to the JS GC. +pub(crate) fn to_js_for_ssr( + this: Box, global_object: &JSGlobalObject, kind: SSRKind, ) -> JSValue { - // SAFETY: caller contract — `this` is a valid exclusive heap allocation. - unsafe { &mut *this }.calculate_estimated_byte_size(); - BakeResponse__createForSSR(global_object, this, kind as u8) + this.calculate_estimated_byte_size(); + BakeResponse__createForSSR(global_object, bun_core::heap::into_raw(this), kind as u8) } // C++ side declares `extern JSC_CALLCONV void* JSC_HOST_CALL_ATTRIBUTES` (SYSV_ABI on win-x64). @@ -132,17 +125,13 @@ pub(crate) fn construct_redirect( if let Some(async_local_storage) = vm.get_dev_server_async_local_storage()? { assert_streaming_disabled(global_this, async_local_storage, b"Response.redirect")?; // Ownership of the allocation transfers to the JS wrapper. - let ptr = bun_core::heap::into_raw(response); - // SAFETY: `ptr` is a fresh heap allocation; JS wrapper adopts it. - return Ok(unsafe { to_js_for_ssr(ptr, global_this, SSRKind::Redirect) }); + return Ok(to_js_for_ssr(response, global_this, SSRKind::Redirect)); } // Ownership of the allocation transfers to the JS wrapper (freed in // `ResponseClass__finalize`). - let ptr = bun_core::heap::into_raw(response); - // SAFETY: `ptr` is a fresh heap allocation; `Response::to_js` hands it to - // the C++ wrapper which owns it thereafter. - Ok(unsafe { &mut *ptr }.to_js(global_this)) + let response: &Response = bun_core::heap::release(response); + Ok(response.to_js(global_this)) } // C++ side declares `extern "C" SYSV_ABI ... JSC_HOST_CALL_ATTRIBUTES`. @@ -196,7 +185,7 @@ pub(crate) fn construct_render( Init { status_code: 200, headers: { - let mut headers = HeadersRef::create_empty(); + let headers = FetchHeaders::create_empty(); headers.put(HTTPHeaderName::Location, &path_str, global_this)?; Some(headers) }, @@ -208,9 +197,7 @@ pub(crate) fn construct_render( )); // Ownership of the allocation transfers to the JS wrapper. - let ptr = bun_core::heap::into_raw(response); - // SAFETY: `ptr` is a fresh heap allocation; JS wrapper adopts it. - let response_js = unsafe { to_js_for_ssr(ptr, global_this, SSRKind::Render) }; + let response_js = to_js_for_ssr(response, global_this, SSRKind::Render); response_js.ensure_still_alive(); Ok(response_js) diff --git a/src/runtime/webcore/Blob.rs b/src/runtime/webcore/Blob.rs index fc5508d0c533..d6422a692257 100644 --- a/src/runtime/webcore/Blob.rs +++ b/src/runtime/webcore/Blob.rs @@ -391,6 +391,9 @@ pub trait BlobExt { fn calculate_estimated_byte_size(&self); fn estimated_size(&self) -> usize; fn to_js(&self, global_object: &JSGlobalObject) -> JSValue; + /// Owning sibling of [`BlobExt::to_js`]: heap-promotes and hands the + /// allocation to the JS wrapper, which frees it via `Blob::finalize`. + fn into_js(self: Box, global_object: &JSGlobalObject) -> JSValue; fn find_or_create_file_from_path( path_or_fd: &mut PathOrFileDescriptor, global_this: &JSGlobalObject, @@ -861,18 +864,17 @@ impl BlobExt for Blob { let mut converter = URLSearchParamsConverter { buf: Vec::new() }; search_params.to_string(&mut converter, URLSearchParamsConverter::convert); let store = Store::init(converter.buf); - // SAFETY: `store` is the sole +1 on this freshly-allocated Store. - unsafe { - (*store.as_ptr()).mime_type = bun_http_types::MimeType::Compact::from( + store.mime_type.set( + bun_http_types::MimeType::Compact::from( // The bare tag, *without* `;charset=UTF-8` (charset promotion is // Compact::to_mime_type's job, applied when read). bun_http_types::MimeType::Table::from_mime_literal( "application/x-www-form-urlencoded", ), ) - .to_mime_type(); - } - let content_type = BlobContentType::from_mime(&store.mime_type); + .to_mime_type(), + ); + let content_type = BlobContentType::from_mime(store.mime_type.get()); let blob = Blob::init_with_store(store, global_this); blob.content_type.set(content_type); @@ -1658,8 +1660,7 @@ impl BlobExt for Blob { let assignment_result: JSValue = webcore::file_sink::JSSink::assign_to_stream( global_this, readable_stream.value, - // SAFETY: file_sink is a live +1 *mut FileSink. - unsafe { &mut *file_sink }, + file_sink, signal_ptr, ); @@ -1932,7 +1933,8 @@ impl BlobExt for Blob { } // `FileSink::to_js` takes its own per-wrapper +1; release init's +1. - let js = sink_mut.to_js(global_this); + // SAFETY: `sink` is the live +1 `*mut FileSink` from `init`. + let js = unsafe { webcore::FileSink::to_js(sink, global_this) }; // SAFETY: `to_js` took a +1; this releases init's +1 (rc ≥ 1 after). unsafe { webcore::FileSink::deref(sink) }; return Ok(js); @@ -1991,7 +1993,7 @@ impl BlobExt for Blob { } // SAFETY: sink is live; `to_js` takes its own per-wrapper +1. - let js = unsafe { (*sink).to_js(global_this) }; + let js = unsafe { webcore::FileSink::to_js(sink, global_this) }; // SAFETY: `to_js` took a +1; rc ≥ 1 after this deref. unsafe { webcore::FileSink::deref(sink) }; Ok(js) @@ -2113,14 +2115,14 @@ impl BlobExt for Blob { } fn get_mime_type(&self) -> Option { - self.store().map(|s| s.mime_type.clone()) + self.store().map(|s| s.mime_type.get().clone()) } fn get_mime_type_or_content_type(&self) -> Option { if self.content_type_was_set.get() { return Some(MimeType::init(self.content_type_slice(), false, None)); } - self.store().map(|s| s.mime_type.clone()) + self.store().map(|s| s.mime_type.get().clone()) } fn get_type(&self, global_this: &JSGlobalObject) -> JSValue { @@ -2129,7 +2131,7 @@ impl BlobExt for Blob { return JscZigString::init(ct).to_js(global_this); } if let Some(store) = self.store.get() { - return JscZigString::init(&store.mime_type.value).to_js(global_this); + return JscZigString::init(&store.mime_type.get().value).to_js(global_this); } JscZigString::EMPTY.to_js(global_this) } @@ -2542,7 +2544,7 @@ impl BlobExt for Blob { // attempt a partial move out of `Store` which has a `Drop` impl. let store = StoreRef::from(Store::new(Store { data: store::Data::Bytes(result), - mime_type: bun_http_types::MimeType::NONE, + mime_type: JsCell::new(bun_http_types::MimeType::NONE), ref_count: bun_ptr::ThreadSafeRefCount::init(), is_all_ascii: None, })); @@ -3717,6 +3719,14 @@ impl BlobExt for Blob { js::to_js_unchecked(global_object, this) } + fn into_js(mut self: Box, global_object: &JSGlobalObject) -> JSValue { + // Same `+1` as `Blob::new`: it belongs to the JS wrapper, whose + // `finalize` owns the allocation from here on. + self.ref_count = bun_ptr::RawRefCount::init(1); + let this: &Self = bun_core::heap::release(self); + this.to_js(global_object) + } + /// `Bun.file(pathOrFd)` core: wrap a path-or-fd in a `Store::File` and /// return a Blob viewing it. Runtime `check_s3` matches the call shape used /// by `server_body.rs` / `fetch.rs` (collapsed from a const generic since @@ -5208,9 +5218,7 @@ pub fn write_file_internal( BodyValue::Error(err_ref) => { let err_js = err_ref.to_js(global_this); destination_blob.detach(); - // SAFETY: `body_value` points into a live JS-heap Body; re-borrowed - // after `err_ref` is consumed so no `&mut` alias remains active. - let _ = unsafe { &mut *body_value }.use_(); + let _ = body_value_ref.use_(); Ok(ControlFlow::Break( JSPromise::dangerously_create_rejected_promise_value_without_notifying_vm( global_this, err_js, @@ -5658,7 +5666,7 @@ pub fn jsdom_file_construct_( name_value_str.to_owned_slice().into_boxed_slice(), )), ref_count: bun_ptr::ThreadSafeRefCount::init(), - mime_type: bun_http_types::MimeType::NONE, + mime_type: JsCell::new(bun_http_types::MimeType::NONE), is_all_ascii: None, })))); } diff --git a/src/runtime/webcore/Body.rs b/src/runtime/webcore/Body.rs index 840e91c38454..03ea5dadd526 100644 --- a/src/runtime/webcore/Body.rs +++ b/src/runtime/webcore/Body.rs @@ -41,28 +41,10 @@ pub(super) fn wtf_impl(s: &WTFStringImpl) -> &WTFStringImplStruct { unsafe { &**s } } -/// Mutable view of a [`Blob`]'s backing `Store` through its -/// `JsCell>` field. Centralises the per-site raw -/// `(*blob.store.get()…as_ptr()).mime_type = …` deref under the same -/// invariant `StoreRef::data_mut` already documents: -/// shared-mutable interior, single-threaded JS event-loop, no concurrent -/// `&Store` outstanding for the borrow's duration. -#[inline] -#[allow(clippy::mut_from_ref)] -fn blob_store_mut(blob: &Blob) -> Option<&mut blob::Store> { - blob.store - .get() - .as_ref() - // SAFETY: `StoreRef` invariant — pointee is a live heap `Store` while - // any `StoreRef` exists; single-threaded JS event-loop discipline - // guarantees no other `&`/`&mut Store` is live for this borrow. - .map(|s| unsafe { &mut *s.as_ptr() }) -} - fn set_blob_content_type(blob: &Blob, mime_type: MimeType) { blob.content_type_was_set.set(true); - if let Some(store) = blob_store_mut(blob) { - store.mime_type = mime_type.clone(); + if let Some(store) = blob.store.get() { + store.mime_type.set(mime_type.clone()); } blob.content_type .set(blob::BlobContentType::from(mime_type)); @@ -74,16 +56,17 @@ fn set_blob_content_type(blob: &Blob, mime_type: MimeType) { // ──────────────────────────────────────────────────────────────────────────── #[inline] -fn as_dom_form_data(value: JSValue) -> Option<*mut DOMFormData> { +fn as_dom_form_data<'a>(value: JSValue) -> Option<&'a mut DOMFormData> { // `DOMFormData` is an opaque C++ type without a `#[bun_jsc::JsClass]` derive; // route through the hand-written `from_js` (`DOMFormData.rs`) instead of // `value.as_::()`. - DOMFormData::from_js(value).map(std::ptr::from_mut::) + DOMFormData::from_js(value) } #[inline] -fn as_url_search_params(value: JSValue) -> Option<*mut URLSearchParams> { +fn as_url_search_params<'a>(value: JSValue) -> Option<&'a mut URLSearchParams> { // See `as_dom_form_data` — opaque C++ type, hand-written `from_js`. - URLSearchParams::from_js(value).map(|p| p.as_ptr()) + // `URLSearchParams` is an opaque ZST FFI handle (S008) — safe deref. + URLSearchParams::from_js(value).map(|p| bun_opaque::opaque_deref_mut(p.as_ptr())) } bun_core::declare_scope!(BodyValue, visible); @@ -678,20 +661,20 @@ impl Value { /// `Body.Value` is not itself a JS class — it lives inside a `Request` or /// `Response` wrapper — so the generic `JSValue::as_::()` path /// cannot be used. Instead, try both wrapper classes and return the inner - /// body pointer. + /// body reference. /// - /// Returns a raw pointer; the storage is owned - /// by the JSC heap cell and outlives the call only as long as `value` is - /// kept alive by the caller. - pub fn from_request_or_response(value: JSValue) -> Option<*mut Value> { + /// The storage is owned by the JSC heap cell and stays live as long as + /// `value` is. Keep the borrow short and do not hold it across a call that + /// re-enters JS and may touch this same body. + pub fn from_request_or_response(value: JSValue) -> Option<&'static mut Value> { if value.is_empty_or_undefined_or_null() { return None; } if let Some(req) = value.as_class_ref::() { - return Some(std::ptr::from_mut::(req.get_body_value())); + return Some(req.get_body_value()); } if let Some(res) = value.as_class_ref::() { - return Some(std::ptr::from_mut::(res.get_body_value())); + return Some(res.get_body_value()); } None } @@ -960,17 +943,16 @@ impl Value { } if let Some(form_data) = as_dom_form_data(value) { - // SAFETY: shim returns a live JSC heap cell. - return Ok(Value::Blob(Blob::from_dom_form_data(global_this, unsafe { - &mut *form_data - }))); + return Ok(Value::Blob(Blob::from_dom_form_data( + global_this, + form_data, + ))); } if let Some(search_params) = as_url_search_params(value) { - // SAFETY: shim returns a live JSC heap cell. return Ok(Value::Blob(Blob::from_url_search_params( global_this, - unsafe { &mut *search_params }, + search_params, ))); } @@ -1015,11 +997,10 @@ impl Value { } match readable.ptr { - webcore::readable_stream::Source::Blob(blob) => { - // SAFETY: `Source::Blob` holds a live *mut ByteBlobLoader for the - // lifetime of the ReadableStream JS wrapper. - let result = if let Some(any_blob) = unsafe { (*blob).to_any_blob(global_this) } - { + webcore::readable_stream::Source::Blob(_) => { + // BACKREF: see `Source::blob()` — payload valid while the stream lives. + let loader = readable.ptr.blob().expect("matched Blob"); + let result = if let Some(any_blob) = loader.to_any_blob(global_this) { match any_blob { AnyBlob::Blob(b) => Value::Blob(b), AnyBlob::InternalBlob(b) => Value::InternalBlob(b), @@ -1072,9 +1053,7 @@ impl Value { &mut self, new: &mut Value, global: &JSGlobalObject, - // Opaque C++ handle, mutated via FFI. Taking - // `NonNull` (not `&`/`&mut`) avoids manufacturing aliased Rust borrows. - headers: Option>, + headers: Option<&FetchHeaders>, ) -> JsTerminated<()> { bun_core::scoped_log!(BodyValue, "resolve"); if let Value::Locked(locked) = self { @@ -1149,12 +1128,10 @@ impl Value { Action::None | Action::GetBlob => { let blob_ptr = Blob::new(new.use_()); // SAFETY: `Blob::new` returns a freshly heap-allocated *mut Blob. - let blob = unsafe { &mut *blob_ptr }; + // Only `&Blob` is needed below (fields are `Cell`/`JsCell`), so no + // `&mut` is live across `to_js`, which re-enters C++/JS. + let blob: &Blob = unsafe { &*blob_ptr }; if let Some(fetch_headers) = headers { - // `headers` is a live C++ FetchHeaders handle; - // `FetchHeaders` is an opaque ZST FFI handle (S008) — safe deref. - let fetch_headers = - bun_opaque::opaque_deref_mut(fetch_headers.as_ptr()); if let Some(content_type) = fetch_headers.fast_get(HTTPHeaderName::ContentType) { @@ -1665,11 +1642,8 @@ pub(crate) trait BodyMixin: BodyOwnerJs + Sized { /// short and do not hold it across a call that re-enters JS. #[allow(clippy::mut_from_ref)] fn get_body_value(&self) -> &mut Value; - /// `FetchHeaders` is an - /// opaque, intrusively-refcounted C++ handle whose accessors take `&mut self` - /// (FFI signature is `*mut`). Returning `NonNull` instead of `&FetchHeaders` - /// avoids deriving `&mut T` from `&T` at the call sites (UB). - fn get_fetch_headers(&self) -> Option>; + /// Borrows the owned handle in `self`; the `+1` stays with the owner. + fn get_fetch_headers(&self) -> Option<&FetchHeaders>; fn get_form_data_encoding(&self) -> JsResult>>; // ──────────────────────────────────────────────────────────────────── @@ -2177,12 +2151,11 @@ pub(crate) trait BodyMixin: BodyOwnerJs + Sized { let value = self.get_body_value(); let blob_ptr = Blob::new(value.use_()); // SAFETY: `Blob::new` returns a freshly heap-allocated, ref-counted Blob. - let blob = unsafe { &mut *blob_ptr }; + // Only `&Blob` is needed below (fields are `Cell`/`JsCell`), so no `&mut` + // is live across `to_js`, which re-enters C++/JS. + let blob: &Blob = unsafe { &*blob_ptr }; if blob.content_type().is_empty() { if let Some(fetch_headers) = BodyMixin::get_fetch_headers(self) { - // `fetch_headers` is a live C++ FetchHeaders handle; - // `FetchHeaders` is an opaque ZST FFI handle (S008) — safe deref. - let fetch_headers = bun_opaque::opaque_deref_mut(fetch_headers.as_ptr()); if let Some(content_type) = fetch_headers.fast_get(HTTPHeaderName::ContentType) { let content_slice = content_type.to_slice(); let mime_type = MimeType::init(content_slice.slice(), true, None); diff --git a/src/runtime/webcore/ByteBlobLoader.rs b/src/runtime/webcore/ByteBlobLoader.rs index dbb592314596..4c1e06242dd8 100644 --- a/src/runtime/webcore/ByteBlobLoader.rs +++ b/src/runtime/webcore/ByteBlobLoader.rs @@ -1,36 +1,41 @@ +use core::cell::Cell; + use bun_collections::VecExt; -use bun_jsc::{JSGlobalObject, JSValue, JsResult}; +use bun_jsc::{JSGlobalObject, JSValue, JsCell, JsResult}; use crate::webcore::blob::store::StoreExt as _; use crate::webcore::blob::{self, Blob, BlobExt as _, StoreRef}; use crate::webcore::readable_stream; use crate::webcore::streams; +/// R-2: reached through a shared `BackRef` from `readable_stream::Source::blob()`, +/// so every field mutated on a JS-reachable path is `Cell` (Copy scalars) or +/// [`JsCell`] (non-Copy) instead of requiring `&mut self`. pub struct ByteBlobLoader { - pub offset: blob::SizeType, + pub offset: Cell, // LIFETIMES.tsv: SHARED — ref() on setup, deref() in clearData - pub store: Option, - pub chunk_size: blob::SizeType, - pub remain: blob::SizeType, - pub done: bool, - pub pulled: bool, + pub store: JsCell>, + pub chunk_size: Cell, + pub remain: Cell, + pub done: Cell, + pub pulled: Cell, /// https://github.com/oven-sh/bun/issues/14988 /// Necessary for converting a ByteBlobLoader from a Blob -> back into a Blob /// Especially for DOMFormData, where the specific content-type might've been serialized into the data. - pub content_type: blob::BlobContentType, + pub content_type: JsCell, } impl Default for ByteBlobLoader { fn default() -> Self { Self { - offset: 0, - store: None, - chunk_size: 1024 * 1024 * 2, - remain: 1024 * 1024 * 2, - done: false, - pulled: false, - content_type: blob::BlobContentType::default(), + offset: Cell::new(0), + store: JsCell::new(None), + chunk_size: Cell::new(1024 * 1024 * 2), + remain: Cell::new(1024 * 1024 * 2), + done: Cell::new(false), + pulled: Cell::new(false), + content_type: JsCell::new(blob::BlobContentType::default()), } } } @@ -85,55 +90,59 @@ impl ByteBlobLoader { blob::BlobContentType::default() }; *self = ByteBlobLoader { - offset, - store: Some(store), - chunk_size: (if user_chunk_size > 0 { - user_chunk_size.min(size) - } else { - size - }) - .min(1024 * 1024 * 2), - remain: size, - done: false, - pulled: false, - content_type, + offset: Cell::new(offset), + store: JsCell::new(Some(store)), + chunk_size: Cell::new( + (if user_chunk_size > 0 { + user_chunk_size.min(size) + } else { + size + }) + .min(1024 * 1024 * 2), + ), + remain: Cell::new(size), + done: Cell::new(false), + pulled: Cell::new(false), + content_type: JsCell::new(content_type), }; } pub fn on_start(&mut self) -> streams::Start { // `streams::BlobSizeType` and `blob::SizeType` are both u64 in the Rust port. - streams::Start::ChunkSize(self.chunk_size) + streams::Start::ChunkSize(self.chunk_size.get()) } pub fn on_pull(&mut self, buffer: &mut [u8], array: JSValue) -> streams::Result { array.ensure_still_alive(); let _keep = bun_jsc::EnsureStillAlive(array); - self.pulled = true; - let Some(store) = self.store.clone() else { + self.pulled.set(true); + let Some(store) = self.store.get().clone() else { return streams::Result::Done; }; - if self.done { + if self.done.get() { return streams::Result::Done; } let temporary = store.shared_view(); - let temporary = &temporary[(self.offset as usize).min(temporary.len())..]; + let temporary = &temporary[(self.offset.get() as usize).min(temporary.len())..]; - let take = buffer.len().min(temporary.len().min(self.remain as usize)); + let take = buffer + .len() + .min(temporary.len().min(self.remain.get() as usize)); let temporary = &temporary[..take]; if temporary.is_empty() { self.clear_data(); - self.done = true; + self.done.set(true); return streams::Result::Done; } let copied = blob::SizeType::try_from(temporary.len()).expect("int cast"); - self.remain = self.remain.saturating_sub(copied); - self.offset = self.offset.saturating_add(copied); + self.remain.set(self.remain.get().saturating_sub(copied)); + self.offset.set(self.offset.get().saturating_add(copied)); debug_assert!(buffer.as_ptr() != temporary.as_ptr()); buffer[..temporary.len()].copy_from_slice(temporary); - if self.remain == 0 { + if self.remain.get() == 0 { return streams::Result::IntoArrayAndDone(streams::IntoArray { value: array, len: copied, @@ -146,10 +155,13 @@ impl ByteBlobLoader { }) } - pub fn to_any_blob(&mut self, global: &JSGlobalObject) -> Option { + pub fn to_any_blob(&self, global: &JSGlobalObject) -> Option { // Take ownership via detach_store() up front. let store = self.detach_store()?; - if self.offset == 0 && self.remain == store.size() && self.content_type.is_empty() { + if self.offset.get() == 0 + && self.remain.get() == store.size() + && self.content_type.get().is_empty() + { // SAFETY: `StoreRef` deref is `&Store`; `to_any_blob` needs `&mut` to move bytes out. // We hold the only outstanding ref (just detached) so exclusive access is sound. if let Some(blob) = unsafe { (*store.as_ptr()).to_any_blob() } { @@ -159,13 +171,13 @@ impl ByteBlobLoader { } let blob = Blob::init_with_store(store, global); - blob.offset.set(self.offset); - blob.size.set(self.remain); + blob.offset.set(self.offset.get()); + blob.size.set(self.remain.get()); // Make sure to preserve the content-type. // https://github.com/oven-sh/bun/issues/14988 - if !self.content_type.is_empty() { - let ct = core::mem::take(&mut self.content_type); + if !self.content_type.get().is_empty() { + let ct = self.content_type.replace(blob::BlobContentType::default()); blob.content_type_was_set.set(!ct.is_empty()); blob.content_type.set(ct); } @@ -174,9 +186,9 @@ impl ByteBlobLoader { Some(blob::Any::Blob(blob)) } - pub fn detach_store(&mut self) -> Option { - if let Some(store) = self.store.take() { - self.done = true; + pub fn detach_store(&self) -> Option { + if let Some(store) = self.store.replace(None) { + self.done.set(true); return Some(store); } None @@ -194,27 +206,35 @@ impl ByteBlobLoader { self.clear_data(); } - fn clear_data(&mut self) { - self.content_type = blob::BlobContentType::default(); + fn clear_data(&self) { + self.content_type.set(blob::BlobContentType::default()); - if let Some(store) = self.store.take() { + if let Some(store) = self.store.replace(None) { drop(store); // store.deref() } } pub fn drain(&mut self) -> Vec { - let Some(store) = self.store.clone() else { + let Some(store) = self.store.get().clone() else { return Vec::new(); }; let temporary = store.shared_view(); - let temporary = &temporary[self.offset as usize..]; - let take = 16384usize.min(temporary.len().min(self.remain as usize)); + let temporary = &temporary[self.offset.get() as usize..]; + let take = 16384usize.min(temporary.len().min(self.remain.get() as usize)); let temporary = &temporary[..take]; // A single owning copy (avoids a `ManuallyDrop` borrow dance). let cloned = Vec::::from_slice(temporary); - self.offset = self.offset.saturating_add(cloned.len() as blob::SizeType); - self.remain = self.remain.saturating_sub(cloned.len() as blob::SizeType); + self.offset.set( + self.offset + .get() + .saturating_add(cloned.len() as blob::SizeType), + ); + self.remain.set( + self.remain + .get() + .saturating_sub(cloned.len() as blob::SizeType), + ); cloned } @@ -241,7 +261,7 @@ impl ByteBlobLoader { pub fn memory_cost(&self) -> usize { // ReadableStreamSource covers @sizeOf(FileReader) - if let Some(store) = &self.store { + if let Some(store) = self.store.get() { return store.memory_cost(); } 0 diff --git a/src/runtime/webcore/FileReader.rs b/src/runtime/webcore/FileReader.rs index 2cb0fba6f932..eadbd3d58e89 100644 --- a/src/runtime/webcore/FileReader.rs +++ b/src/runtime/webcore/FileReader.rs @@ -1,4 +1,4 @@ -use core::cell::{Cell, UnsafeCell}; +use core::cell::Cell; use core::mem; use bun_collections::VecExt; @@ -31,14 +31,10 @@ bun_core::declare_scope!(FileReader, visible); // `JsCell` are both `#[repr(transparent)]`, so the embedded layout (offset // 0 of `NewSource`) is unchanged. pub struct FileReader { - /// Wrapped in `UnsafeCell` so that the back-ref `*mut FileReader` (vtable - /// `parent`) and the reader's own `&mut self` both derive from a - /// SharedReadWrite root — see `BufferedReaderParent` aliasing contract - /// (PipeReader.rs). The vtable callbacks fire while a `&mut BufferedReader` - /// is live on the caller's stack and re-enter `self.reader` (close/buffer/ - /// is_done); without `UnsafeCell` materializing `&mut FileReader` there is - /// Stacked-Borrows UB. Matches sibling `IOReader` (shell) port. - pub reader: UnsafeCell, + /// `JsCell` so the vtable back-ref `*mut FileReader` and the reader's own + /// `&mut self` both derive from one SharedReadWrite root: the callbacks fire + /// with a `&mut BufferedReader` live and re-enter `self.reader`. + pub reader: JsCell, pub done: Cell, pub pending: JsCell, pub pending_value: JsCell, // Strong.Optional @@ -68,7 +64,7 @@ pub struct FileReader { impl Default for FileReader { fn default() -> Self { Self { - reader: UnsafeCell::new(IOReader::init::()), + reader: JsCell::new(IOReader::init::()), done: Cell::new(false), pending: JsCell::new(streams::Pending::default()), pending_value: JsCell::new(Strong::empty()), @@ -274,11 +270,10 @@ impl Lazy { // BufferedReader vtable parent: wires the // `onReadChunk`/`onReaderDone`/`onReaderError`/`loop`/`eventLoop` callbacks. // -// R-2: every mutated field on `FileReader` is `Cell`/`JsCell`/`UnsafeCell`- -// backed, so materializing `&FileReader` via `(&*this)` does not assert Unique -// over any byte the caller may have borrowed (SharedReadWrite root); the -// inherent impls re-derive any reader access through `reader()` -// (`UnsafeCell::get`). +// R-2: every mutated field on `FileReader` is `Cell`/`JsCell`-backed, so +// materializing `&FileReader` via `(&*this)` does not assert Unique over any +// byte the caller may have borrowed (SharedReadWrite root); the inherent impls +// re-derive any reader access through `reader()` (`JsCell::get_mut`). bun_io::impl_buffered_reader_parent! { FileReader for FileReader; has_on_read_chunk = true; @@ -297,20 +292,16 @@ bun_io::impl_buffered_reader_parent! { } impl FileReader { - /// SharedReadWrite accessor for the embedded `BufferedReader`. See the - /// `UnsafeCell` note on the field declaration — this is the single point - /// through which all `self.reader` access flows so vtable-callback + /// SharedReadWrite accessor for the embedded `BufferedReader` — the single + /// point through which all `self.reader` access flows, so vtable-callback /// re-entrancy and outer `&mut FileReader` borrows both root at the cell. - /// SAFETY: single-threaded (JS event loop); the cell is the sole - /// SharedReadWrite root — see the unsafe block below. #[inline] #[allow(clippy::mut_from_ref)] pub fn reader(&self) -> &mut IOReader { - // SAFETY: `FileReader` is single-threaded (JS event loop) and every - // `self.reader` access flows through this accessor, so the `UnsafeCell` - // is the sole SharedReadWrite root — no `&mut IOReader` is held live - // across a vtable-callback re-entry point (see field doc comment). - unsafe { &mut *self.reader.get() } + // SAFETY: single JS thread; every access re-derives through this + // accessor and no `&mut IOReader` is held live across a vtable-callback + // re-entry point (see field doc comment). + unsafe { self.reader.get_mut() } } pub fn event_loop(&self) -> EventLoopHandle { @@ -329,7 +320,7 @@ impl FileReader { // host-fn could re-enter; `*self =` requires unique access. pub fn setup(&mut self, fd: Fd) { *self = FileReader { - reader: UnsafeCell::new(IOReader::init::()), + reader: JsCell::new(IOReader::init::()), done: Cell::new(false), fd: Cell::new(fd), ..Default::default() diff --git a/src/runtime/webcore/FileSink.rs b/src/runtime/webcore/FileSink.rs index 4a17e701fc50..c95dd08646ee 100644 --- a/src/runtime/webcore/FileSink.rs +++ b/src/runtime/webcore/FileSink.rs @@ -743,17 +743,17 @@ impl FileSink { } /// `EventLoopHandle::bun_vm()` returns an erased `*mut ()`; recover the - /// typed `&mut VirtualMachine` (None for the mini loop or null). + /// typed `&VirtualMachine` (None for the mini loop or null). #[inline] - #[allow(clippy::mut_from_ref)] // recovers `&mut` from a type-erased raw ptr (per-thread VM, not aliased) - fn js_vm(&self) -> Option<&mut bun_jsc::VirtualMachineRef> { + fn js_vm(&self) -> Option<&'static bun_jsc::VirtualMachineRef> { let p = self.event_loop_handle.bun_vm(); if p.is_null() { return None; } - // SAFETY: `bun_vm()` returns an erased `*mut VirtualMachine` for the - // Js arm; non-null implies the per-thread VM, never aliased here. - Some(unsafe { &mut *p.cast::() }) + // SAFETY: non-null means the Js arm's per-thread VM, a singleton that + // outlives every sink; it is aliased by `VirtualMachine::get()`, so + // hand out `&` and mutate through its `Cell`/`JsCell` fields. + Some(unsafe { &*p.cast::() }) } pub fn connect(&self, signal: streams::Signal) { @@ -1041,26 +1041,34 @@ impl FileSink { /// and the caller must hold the last reference. unsafe fn deinit(this: *mut FileSink) { LIVE_COUNT.fetch_sub(1, Ordering::Relaxed); - // SAFETY: caller contract — `this` is valid and uniquely owned. - let self_ = unsafe { &mut *this }; + // SAFETY: caller contract — `this` came from `heap::alloc` in the + // constructors and the caller holds the last reference. + let self_ = unsafe { bun_core::heap::take(this) }; // pending/readable_stream/js_sink_ref are dropped by Box drop below. if let Some(global) = self_.js_global() { - // SAFETY: `bun_vm()` is non-null when `js_global()` returned Some. let vm = global.bun_vm().as_mut(); - AutoFlusher::unregister_deferred_microtask_with_type::(self_, vm); + AutoFlusher::unregister_deferred_microtask_with_type::(&*self_, vm); } - // SAFETY: `this` was produced by `heap::alloc` in the constructors. - drop(unsafe { bun_core::heap::take(this) }); + drop(self_); } - pub fn to_js(&mut self, global_this: &JSGlobalObject) -> JSValue { + /// Takes the canonical `*mut FileSink` (not `&mut self`): the wrapper stashes + /// the pointer and later writes and `deref()`s through it, so it must keep + /// full write+dealloc provenance. See the `borrow = ptr` note above. + /// + /// # Safety + /// `this` must point to a live `FileSink` from `create*`/`init`. + pub unsafe fn to_js(this: *mut FileSink, global_this: &JSGlobalObject) -> JSValue { // Wrapper's +1; balanced by `finalize` → `deref()`. - self.ref_(); - JSSink::create_object(global_this, self, 0) + // SAFETY: caller contract — `this` is live; `ref_` only touches `Cell`. + unsafe { (*this).ref_() }; + JSSink::create_object(global_this, this, 0) } - pub fn to_js_with_destructor( - &mut self, + /// # Safety + /// Same contract as [`to_js`](Self::to_js). + pub unsafe fn to_js_with_destructor( + this: *mut FileSink, global_this: &JSGlobalObject, // `sink::DestructorPtr` is `TaggedPtrUnion<(Detached, Detached)>` // which does not satisfy `bun_ptr::TypeList` yet (sibling Sink.rs); accept @@ -1068,8 +1076,9 @@ impl FileSink { destructor: Option, ) -> JSValue { // Wrapper's +1; balanced by `finalize` → `deref()`. - self.ref_(); - JSSink::create_object(global_this, self, destructor.unwrap_or(0)) + // SAFETY: caller contract — `this` is live; `ref_` only touches `Cell`. + unsafe { (*this).ref_() }; + JSSink::create_object(global_this, this, destructor.unwrap_or(0)) } pub fn end_from_js(&self, global_this: &JSGlobalObject) -> sys::Result { @@ -1395,84 +1404,95 @@ fn on_reject_stream(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsRe } impl FileSink { - pub fn assign_to_stream( - &mut self, + /// Takes the canonical `*mut FileSink` (not `&mut self`): the JS builtin + /// re-enters the sink through the C++-held pointer, and `_guard`/`then()` + /// may drop the last ref, so no `&`/`&mut FileSink` may span those calls. + /// + /// # Safety + /// `this` must point to a live `FileSink` with write+dealloc provenance. + pub unsafe fn assign_to_stream( + this: *mut FileSink, stream: &mut ReadableStream, global_this: &JSGlobalObject, ) -> JSValue { - self.signal.set(SinkSignal::init(JSValue::ZERO)); - // SAFETY: `&mut self` carries write+dealloc provenance over the allocation. - let _guard = unsafe { FileSinkRef::new_ref(std::ptr::from_mut::(self)) }; - - // explicitly set it to a dead pointer - // we use this memory address to disable signals being sent - self.signal.with_mut(|s| s.clear()); - - self.readable_stream - .set(readable_stream::Strong::init(*stream, global_this)); - // reshaped for borrowck — re-derive `signal_ptr` after - // assigning `readable_stream`. `JsCell::as_ptr` yields the stable - // address of the inner `Signal` (`#[repr(transparent)]` over - // `UnsafeCell`). - // SAFETY: project to `signal.ptr` without forming a reference; - // `Option>` is ABI-identical to `*mut c_void` (see - // const-asserts on `Signal` in streams.rs), so FFI may write the - // JSValue bits back through this `void**`. - let signal_ptr: *mut *mut c_void = - unsafe { (&raw mut (*self.signal.as_ptr()).ptr).cast::<*mut c_void>() }; - // No per-wrapper +1 for the controller (only the transient `_guard` - // above): the JS builtins always call `controller.end()`/`.close()` - // (`${controller}__end/close` → `controller->detach()` → m_sinkPtr=null) - // before GC, so the controller's dtor never reaches `finalize`. - let promise_result = JSSink::assign_to_stream(global_this, stream.value, self, signal_ptr); - - if let Some(err) = promise_result.to_error() { - self.readable_stream.set(readable_stream::Strong::default()); - return err; - } + // SAFETY: caller contract — `this` is live; every statement reborrows a + // single field and holds no borrow across a re-entrant/freeing call. + unsafe { + (*this).signal.set(SinkSignal::init(JSValue::ZERO)); + let _guard = FileSinkRef::new_ref(this); - if !promise_result.is_empty_or_undefined_or_null() { - if let Some(promise) = promise_result.as_any_promise() { - // `bun_jsc::AnyPromise` (the active raw-ptr variant in - // lib.rs) does not yet expose `status()`/`result()`; recover the - // underlying `JSPromise` (JSInternalPromise subclasses JSPromise - // in C++, so the cast is layout-safe). - let js_promise: *mut bun_jsc::JSPromise = match promise { - bun_jsc::AnyPromise::Normal(p) => p, - bun_jsc::AnyPromise::Internal(p) => p.cast::(), - }; - // SAFETY: `as_any_promise` returned non-null. - match unsafe { (*js_promise).status() } { - bun_jsc::js_promise::Status::Pending => { - self.writer - .with_mut(|w| w.enable_keeping_process_alive(self.io_evtloop())); - self.ref_(); - // TODO: properly propagate exception upwards - // `JSValue::then` takes already-wrapped C-ABI - // host fns; the `toJSHostFunction` step is the manual - // shims at the bottom of this file. - promise_result.then( - global_this, - std::ptr::from_mut::(self), - on_resolve_stream_shim, - on_reject_stream_shim, - ); - } - bun_jsc::js_promise::Status::Fulfilled => { - // These don't ref(). - self.handle_resolve_stream(global_this); - } - bun_jsc::js_promise::Status::Rejected => { - // These don't ref(). - // SAFETY: `js_promise` is non-null (`as_any_promise`). - let result = unsafe { (*js_promise).result(global_this.vm()) }; - self.handle_reject_stream(global_this, result); + // explicitly set it to a dead pointer + // we use this memory address to disable signals being sent + (*this).signal.with_mut(|s| s.clear()); + + (*this) + .readable_stream + .set(readable_stream::Strong::init(*stream, global_this)); + // Project to `signal.ptr` without forming a reference; + // `Option>` is ABI-identical to `*mut c_void` (see + // const-asserts on `Signal` in streams.rs), so FFI may write the + // JSValue bits back through this `void**`. + let signal_ptr: *mut *mut c_void = + (&raw mut (*(*this).signal.as_ptr()).ptr).cast::<*mut c_void>(); + // No per-wrapper +1 for the controller (only the transient `_guard` + // above): the JS builtins always call `controller.end()`/`.close()` + // (`${controller}__end/close` → `controller->detach()` → m_sinkPtr=null) + // before GC, so the controller's dtor never reaches `finalize`. + let promise_result = + JSSink::assign_to_stream(global_this, stream.value, this, signal_ptr); + + if let Some(err) = promise_result.to_error() { + (*this) + .readable_stream + .set(readable_stream::Strong::default()); + return err; + } + + if !promise_result.is_empty_or_undefined_or_null() { + if let Some(promise) = promise_result.as_any_promise() { + // `bun_jsc::AnyPromise` (the active raw-ptr variant in + // lib.rs) does not yet expose `status()`/`result()`; recover the + // underlying `JSPromise` (JSInternalPromise subclasses JSPromise + // in C++, so the cast is layout-safe). + let js_promise: *mut bun_jsc::JSPromise = match promise { + bun_jsc::AnyPromise::Normal(p) => p, + bun_jsc::AnyPromise::Internal(p) => p.cast::(), + }; + match (*js_promise).status() { + bun_jsc::js_promise::Status::Pending => { + // Read the handle out first: the `with_mut` closure must + // not touch `*this` again while `&mut IOWriter` is live. + let evtloop = (*this).io_evtloop(); + (*this) + .writer + .with_mut(|w| w.enable_keeping_process_alive(evtloop)); + (*this).ref_(); + // TODO: properly propagate exception upwards + // `JSValue::then` takes already-wrapped C-ABI + // host fns; the `toJSHostFunction` step is the manual + // shims at the bottom of this file. + promise_result.then( + global_this, + this, + on_resolve_stream_shim, + on_reject_stream_shim, + ); + } + bun_jsc::js_promise::Status::Fulfilled => { + // These don't ref(). + (*this).handle_resolve_stream(global_this); + } + bun_jsc::js_promise::Status::Rejected => { + // These don't ref(). + let result = (*js_promise).result(global_this.vm()); + (*this).handle_reject_stream(global_this, result); + } } } } - } - promise_result + promise_result + } } } diff --git a/src/runtime/webcore/ObjectURLRegistry.rs b/src/runtime/webcore/ObjectURLRegistry.rs index c24d351528b8..49d934bde870 100644 --- a/src/runtime/webcore/ObjectURLRegistry.rs +++ b/src/runtime/webcore/ObjectURLRegistry.rs @@ -117,8 +117,7 @@ pub(crate) fn bun_create_object_url( .throw_invalid_arguments(format_args!("createObjectURL expects a Blob object"))); }; let registry = ObjectURLRegistry::singleton(); - // SAFETY: `bun_vm_ptr()` returns the live VM pointer for `global_object`. - let uuid = registry.register(unsafe { &mut *global_object.bun_vm_ptr() }, blob); + let uuid = registry.register(global_object.bun_vm().as_mut(), blob); let mut str = bun_core::String::create_format(format_args!("blob:{}", uuid)); str.transfer_to_js(global_object) } diff --git a/src/runtime/webcore/ReadableStream.rs b/src/runtime/webcore/ReadableStream.rs index b130fb9dc77c..585863c3dbee 100644 --- a/src/runtime/webcore/ReadableStream.rs +++ b/src/runtime/webcore/ReadableStream.rs @@ -171,9 +171,9 @@ impl ReadableStream { let _ = self.reload_tag(global_this); match self.ptr { - Source::Blob(blobby) => { - // SAFETY: ptr came from ReadableStreamTag__tagged; valid while stream alive. - let blobby = unsafe { &mut *blobby }; + Source::Blob(_) => { + // BACKREF: see `Source::blob()` — payload valid while stream alive. + let blobby = self.ptr.blob().expect("matched Blob"); if let Some(blob) = blobby.to_any_blob(global_this) { self.done(global_this); return Some(blob); @@ -578,6 +578,23 @@ impl Source { _ => None, } } + + /// Shared borrow of the `Blob` payload as a [`BackRef`](bun_ptr::BackRef). + /// + /// Same invariant as [`bytes`](Self::bytes): the pointer is the JS + /// wrapper's `m_ctx` heap allocation, non-null and live while the owning + /// `ReadableStream` JSValue is rooted. R-2: every `ByteBlobLoader` field + /// touched through this borrow is `Cell`/`JsCell`-backed, so re-entrant JS + /// that re-derives a fresh `&ByteBlobLoader` from `m_ctx` aliases shared-only. + #[inline] + pub fn blob(self) -> Option> { + match self { + Source::Blob(p) => Some(bun_ptr::BackRef::from( + NonNull::new(p).expect("Source::Blob payload is non-null"), + )), + _ => None, + } + } } // ─── NewSource ─────────────────────────────────────────────────────────────── @@ -912,9 +929,9 @@ impl NewSource { } if let Some(close) = self.close_handler.take() { // Identity check against the *exact* fn pointer stored by `set_on_close_from_js`, so the - // JS path receives `self` (not `close_ctx`, which is unset on that path). + // JS path calls the safe method directly (`close_ctx` is unset on that path). if close as usize == Self::on_js_close as fn(Option<*mut c_void>) as usize { - Self::on_js_close(Some(std::ptr::from_mut(self).cast::())); + self.on_close_from_js(); } else { close(self.close_ctx.map(|p| p.as_ptr())); } @@ -926,16 +943,22 @@ impl NewSource { /// `close_handler` by [`Self::set_on_close_from_js`] so the fn-pointer /// identity check above matches. fn on_js_close(ptr: Option<*mut c_void>) { - // SAFETY: ptr was set to `self as *mut NewSource` in on_close()/set_on_close_from_js. - let this = unsafe { &mut *(ptr.unwrap().cast::>()) }; + // Typed trampoline: one shared deref, then a `&self` method. No `&mut` + // is formed, so the `queue_microtask` below cannot stack a second borrow. + // SAFETY: ptr is the `*mut NewSource` stored by `set_on_close_from_js`. + let this = unsafe { &*(ptr.unwrap().cast::>()) }; + this.on_close_from_js(); + } + + fn on_close_from_js(&self) { // Reached from `FileReader::on_reader_done` off the event loop. While // the across-read ref is held (`increment_count` upgraded to Strong), // the wrapper is rooted and `try_get()` is `Some`. If the wrapper was // already finalized, `try_get()` is `None` and there is no callback. - let Some(this_jsvalue) = this.this_jsvalue.try_get() else { + let Some(this_jsvalue) = self.this_jsvalue.try_get() else { return; }; - let global_this = this.global_this(); + let global_this = self.global_this(); if let Some(cb) = ::on_close_callback_get_cached(this_jsvalue) { if !cb.is_undefined() { global_this.queue_microtask(cb, &[]); diff --git a/src/runtime/webcore/Request.rs b/src/runtime/webcore/Request.rs index 0e2f9e667c8c..89f371f07209 100644 --- a/src/runtime/webcore/Request.rs +++ b/src/runtime/webcore/Request.rs @@ -8,7 +8,7 @@ use std::borrow::Cow; use bun_jsc::JsCell; use enumset::EnumSet; -use super::response::HeadersRef; +use super::response::FetchHeaders; use crate::api::AnyRequestContext; use crate::webcore::BlobExt as _; use crate::webcore::blob::ZigStringBlobExt as _; @@ -16,7 +16,7 @@ use crate::webcore::body::{self, BodyHiveHandle, BodyMixin, Value as BodyValue}; use crate::webcore::jsc::{ self as jsc, CallFrame, HTTPHeaderName, JSGlobalObject, JSValue, JsError, JsRef, JsResult, }; -use crate::webcore::{AbortSignal, Blob, CookieMap, FetchHeaders, ReadableStream, Response}; +use crate::webcore::{AbortSignal, Blob, CookieMap, ReadableStream, Response}; use bun_alloc::AllocError; use bun_core::{Output, fmt as bun_fmt}; use bun_core::{OwnedStringCell, String as BunString, ZigString, strings}; @@ -36,9 +36,9 @@ use bun_uws as uws; use core::mem::ManuallyDrop; impl bun_ptr::weak_ptr::HasWeakPtrData for Request { - unsafe fn weak_ptr_data(this: *mut Self) -> *mut WeakPtrData { + unsafe fn weak_ptr_data(this: *mut Self) -> *const Cell { // SAFETY: caller guarantees `this` points to a live (possibly-finalized) allocation. - unsafe { core::ptr::addr_of_mut!((*this).weak_ptr_data) } + unsafe { core::ptr::addr_of!((*this).weak_ptr_data) } } } pub(crate) type WeakRef = bun_ptr::WeakPtr; @@ -78,14 +78,13 @@ const _: () = { /// `&mut Request`) so re-entrant JS calls cannot stack two `&mut` to the same /// instance. Fields mutated by host-fns are wrapped in `Cell` (Copy scalars) /// or `JsCell` (Drop types). Both are `#[repr(transparent)]`, so `#[repr(C)]` -/// field layout is unchanged. `method`/`flags`/`request_context`/`body`/ -/// `weak_ptr_data` are only written during construction or via raw-ptr -/// `finalize`, so stay plain. +/// field layout is unchanged. `method`/`flags`/`request_context`/`body` are +/// only written during construction or via raw-ptr `finalize`, so stay plain. #[repr(C)] pub struct Request { pub url: bun_core::OwnedStringCell, - headers: JsCell>, + headers: JsCell>, // AbortSignal is an opaque C++ handle with intrusive WebCore refcounting — // `Arc` of an opaque ZST is meaningless (its payload address is not the // C++ object). `AbortSignalRef` wraps `NonNull` and routes @@ -101,7 +100,7 @@ pub struct Request { pub method: Method, pub flags: Flags, pub request_context: AnyRequestContext, - pub weak_ptr_data: WeakPtrData, + pub weak_ptr_data: Cell, // We must report a consistent value for this pub reported_estimated_size: Cell, pub internal_event_callback: JsCell, @@ -177,15 +176,8 @@ impl BodyMixin for Request { Request::get_body_value(self) } #[inline] - fn get_fetch_headers(&self) -> Option> { - // Opaque C++ handle. Return the raw `*mut` - // directly (via `HeadersRef::as_ptr`) so the provenance is mutable; - // going through `as_deref()` would derive it from a `&FetchHeaders` - // and make the later `as_mut()` UB under Stacked Borrows. - self.headers.get().as_ref().map(|h| { - core::ptr::NonNull::new(h.as_ptr()) - .expect("HeadersRef wraps a non-null *mut FetchHeaders") - }) + fn get_fetch_headers(&self) -> Option<&FetchHeaders> { + self.headers.get().as_ref() } #[inline] fn get_form_data_encoding( @@ -220,15 +212,11 @@ impl Request { unsafe { &mut (*self.body.as_ptr()).value } } - /// R-2: short-hand for `unsafe { self.headers.get_mut() }`. The - /// single-JS-thread invariant (see `JsCell` docs) means no other - /// `&mut Option` is live for the duration of the borrow. + /// Shared accessor over the C++ handle; mirrored by `Response::headers`. + /// Every `FetchHeaders` method takes `&self`, so this reaches all of them. #[inline] - #[allow(clippy::mut_from_ref)] - fn headers_mut(&self) -> &mut Option { - // SAFETY: single-JS-thread; callers below keep the borrow short and do - // not re-enter a path that touches `self.headers`. - unsafe { self.headers.get_mut() } + pub fn headers(&self) -> Option<&FetchHeaders> { + self.headers.get().as_ref() } // Returns if the request has headers already cached/set. @@ -238,29 +226,29 @@ impl Request { /// Sets the headers of the request. This will take ownership of the headers. /// it will deref the previous headers if they exist. - pub fn set_fetch_headers(&self, headers: Option) { - // old_headers.deref() → handled by HeadersRef::Drop on assignment + pub fn set_fetch_headers(&self, headers: Option) { + // old_headers.deref() → handled by FetchHeaders::Drop on assignment self.headers.set(headers); } /// Returns the headers of the request. If the headers are not already cached, it will create a new FetchHeaders object. /// If the headers are empty, it will look at request_context to get the headers. /// If the headers are empty and request_context is null, it will create an empty FetchHeaders object. - #[allow(clippy::mut_from_ref)] - pub fn ensure_fetch_headers(&self, global_this: &JSGlobalObject) -> JsResult<&mut HeadersRef> { + pub fn ensure_fetch_headers(&self, global_this: &JSGlobalObject) -> JsResult<&FetchHeaders> { if self.headers.get().is_some() { // headers is already set - return Ok(self.headers_mut().as_mut().unwrap()); + return Ok(self.headers().unwrap()); } if let Some(req) = self.request_context.get_request() { // we have a request context, so we can get the headers from it - self.headers.set(Some(HeadersRef::create_from_uws( - req.cast::(), - ))); + // SAFETY: `req` is the live `uWS::HttpRequest` held by our request context. + self.headers.set(Some(unsafe { + FetchHeaders::create_from_uws(req.cast::()) + })); } else { // we don't have a request context, so we need to create an empty headers object - self.headers.set(Some(HeadersRef::create_empty())); + self.headers.set(Some(FetchHeaders::create_empty())); // Snapshot the pointer first; it stays valid across the field borrow. let content_type: Option<*const [u8]> = match self.body_value() { BodyValue::Blob(blob) => { @@ -268,11 +256,12 @@ impl Request { } BodyValue::Locked(locked) => match locked.readable.get(global_this) { Some(readable) => match readable.ptr { - crate::webcore::readable_stream::Source::Blob(blob) => { - // SAFETY: `Source::Blob` holds a live `*mut ByteBlobLoader` - // for as long as the readable stream exists; we only read - // its `content_type` slice and immediately copy below. - let ct: &[u8] = unsafe { (*blob).content_type.as_slice() }; + crate::webcore::readable_stream::Source::Blob(_) => { + // BACKREF: see `Source::blob()` — payload valid while the + // stream lives; we only read its `content_type` slice and + // immediately copy below. + let loader = readable.ptr.blob().expect("matched Blob"); + let ct: &[u8] = loader.content_type.get().as_slice(); Some(std::ptr::from_ref::<[u8]>(ct)) } _ => None, @@ -287,7 +276,7 @@ impl Request { // call; the bytes are copied into the header map below. let content_type_ = unsafe { &*content_type_ }; if !content_type_.is_empty() { - self.headers_mut().as_mut().unwrap().put( + self.headers().unwrap().put( HTTPHeaderName::ContentType, &BunString::ascii(content_type_), global_this, @@ -296,21 +285,21 @@ impl Request { } } - Ok(self.headers_mut().as_mut().unwrap()) + Ok(self.headers().unwrap()) } - #[allow(clippy::mut_from_ref)] - pub fn get_fetch_headers_unless_empty(&self) -> Option<&mut HeadersRef> { + pub fn get_fetch_headers_unless_empty(&self) -> Option<&FetchHeaders> { if self.headers.get().is_none() { if let Some(req) = self.request_context.get_request() { // we have a request context, so we can get the headers from it - self.headers.set(Some(HeadersRef::create_from_uws( - req.cast::(), - ))); + // SAFETY: `req` is the live `uWS::HttpRequest` held by our request context. + self.headers.set(Some(unsafe { + FetchHeaders::create_from_uws(req.cast::()) + })); } } - let headers = self.headers_mut().as_mut()?; + let headers = self.headers.get().as_ref()?; if headers.is_empty() { return None; } @@ -322,16 +311,17 @@ impl Request { Ok(self.ensure_fetch_headers(global_this)?.to_js(global_this)) } - pub fn clone_headers(&self, global_this: &JSGlobalObject) -> JsResult> { + pub fn clone_headers(&self, global_this: &JSGlobalObject) -> JsResult> { if self.headers.get().is_none() { if let Some(uws_req) = self.request_context.get_request() { - self.headers.set(Some(HeadersRef::create_from_uws( - uws_req.cast::(), - ))); + // SAFETY: `uws_req` is the live `uWS::HttpRequest` held by our request context. + self.headers.set(Some(unsafe { + FetchHeaders::create_from_uws(uws_req.cast::()) + })); } } - if let Some(head) = self.headers_mut().as_mut() { + if let Some(head) = self.headers.get().as_ref() { if head.is_empty() { return Ok(None); } @@ -351,7 +341,7 @@ impl Request { } } - if let Some(headers) = self.headers_mut().as_mut() { + if let Some(headers) = self.headers() { if let Some(value) = headers.fast_get(HTTPHeaderName::ContentType) { return Ok(Some(value.to_slice())); } @@ -451,7 +441,7 @@ impl Request { /// TODO: do we need this? pub fn init2( url: BunString, - headers: Option, + headers: Option, body: BodyHiveHandle, method: Method, ) -> Request { @@ -464,7 +454,7 @@ impl Request { method, flags: Flags::default(), request_context: AnyRequestContext::NULL, - weak_ptr_data: WeakPtrData::EMPTY, + weak_ptr_data: Cell::new(WeakPtrData::EMPTY), reported_estimated_size: Cell::new(0), internal_event_callback: JsCell::new(InternalJSEventCallback::default()), } @@ -724,7 +714,7 @@ impl Request { } pub fn mime_type(&self) -> &[u8] { - if let Some(headers) = self.headers_mut().as_mut() { + if let Some(headers) = self.headers() { if let Some(content_type) = headers.fast_get(HTTPHeaderName::ContentType) { // `fast_get` returns a `ZigString` by value whose // bytes borrow the FetchHeaders' WTF::String storage (NOT the @@ -803,7 +793,7 @@ impl Request { } pub fn finalize_without_deinit(&mut self) { - // headers.deref() → HeadersRef::Drop when set to None + // headers.deref() → FetchHeaders::Drop when set to None self.headers.set(None); self.url.set(BunString::empty()); @@ -824,7 +814,10 @@ impl Request { // hot-path `Box::from_raw().drop()` below cannot re-run this. // SAFETY: `this` is live and this is the sole release point for `body`. unsafe { ManuallyDrop::drop(&mut this.body) }; - if this.weak_ptr_data.on_finalize() { + let mut weak_data = this.weak_ptr_data.get(); + let last_ref = weak_data.on_finalize(); + this.weak_ptr_data.set(weak_data); + if last_ref { // Hot path: no outstanding weak refs. Reclaim and drop the whole // allocation in one shot — `Box::from_raw`'s drop runs // `drop_in_place` over every field (headers / url / signal / @@ -847,7 +840,7 @@ impl Request { } pub fn get_referrer(&self, global_object: &JSGlobalObject) -> JSValue { - if let Some(headers_ref) = self.headers_mut().as_mut() { + if let Some(headers_ref) = self.headers() { if let Some(referrer) = headers_ref.get(b"referrer", global_object) { return referrer.to_js(global_object); } @@ -1097,7 +1090,7 @@ impl Request { method: Method::GET, flags: Flags::default(), request_context: AnyRequestContext::NULL, - weak_ptr_data: WeakPtrData::EMPTY, + weak_ptr_data: Cell::new(WeakPtrData::EMPTY), reported_estimated_size: Cell::new(0), internal_event_callback: JsCell::new(InternalJSEventCallback::default()), }; @@ -1258,22 +1251,22 @@ impl Request { } if let Some(response) = value.as_direct::() { - // SAFETY: `as_direct` returned a live `*mut Response` owned by the JS wrapper. - let response = unsafe { &mut *response }; + // SAFETY: as_direct returns a live *mut Response payload (m_ctx) + let response = unsafe { &*response }; if !fields.contains(Fields::Method) { req.method = response.get_method(); fields.insert(Fields::Method); } if !fields.contains(Fields::Headers) { - if let Some(headers) = response.get_init_headers_mut() { + if let Some(headers) = response.headers() { // The flag is set unconditionally once `getInitHeaders()` yielded a // value, even if `cloneThis` returns null — so a later arg can't // repopulate headers from a different source. match headers.clone_this(global_this) { Ok(h) => { - // SAFETY: clone_this returns a +1 ref FetchHeaders. - req.headers.set(h.map(|p| unsafe { HeadersRef::adopt(p) })); + // `clone_this` hands back an owned +1; the cell takes it. + req.headers.set(h); fields.insert(Fields::Headers); } Err(e) => bail!(Err(e)), @@ -1549,19 +1542,10 @@ impl Request { if matches!(req.body_value(), BodyValue::Blob(_)) && req.headers.get().is_some() { if let BodyValue::Blob(blob) = req.body_value() { let ct: &[u8] = blob.content_type_slice(); - if !ct.is_empty() - && !req - .headers_mut() - .as_mut() - .unwrap() - .fast_has(HTTPHeaderName::ContentType) - { - // Reshaped for borrowck — split borrow of req.body and req.headers - let ct_ptr: *const [u8] = ct; - match req.headers_mut().as_mut().unwrap().put( + if !ct.is_empty() && !req.headers().unwrap().fast_has(HTTPHeaderName::ContentType) { + match req.headers().unwrap().put( HTTPHeaderName::ContentType, - // SAFETY: ct_ptr borrows req.body which is not mutated here. - &BunString::ascii(unsafe { &*ct_ptr }), + &BunString::ascii(ct), global_this, ) { Ok(()) => {} @@ -1657,7 +1641,7 @@ impl Request { method: self.method, flags: self.flags, request_context: AnyRequestContext::NULL, - weak_ptr_data: WeakPtrData::EMPTY, + weak_ptr_data: Cell::new(WeakPtrData::EMPTY), reported_estimated_size: Cell::new(0), internal_event_callback: JsCell::new(InternalJSEventCallback::default()), }, @@ -1690,7 +1674,7 @@ impl Request { method: Method::GET, flags: Flags::default(), request_context: AnyRequestContext::NULL, - weak_ptr_data: WeakPtrData::EMPTY, + weak_ptr_data: Cell::new(WeakPtrData::EMPTY), reported_estimated_size: Cell::new(0), internal_event_callback: JsCell::new(InternalJSEventCallback::default()), }); @@ -1762,23 +1746,9 @@ impl Request { ..Flags::default() }, request_context, - weak_ptr_data: WeakPtrData::EMPTY, + weak_ptr_data: Cell::new(WeakPtrData::EMPTY), reported_estimated_size: Cell::new(0), internal_event_callback: JsCell::new(InternalJSEventCallback::default()), } } - - #[inline] - pub fn get_fetch_headers(&self) -> Option<&FetchHeaders> { - self.headers.get().as_deref() - } - - /// Mutable access to the already-materialized headers (does NOT lazily - /// create from the underlying uWS request — see `get_fetch_headers_unless_empty` - /// for that). - #[inline] - #[allow(clippy::mut_from_ref)] - pub fn get_fetch_headers_mut(&self) -> Option<&mut FetchHeaders> { - self.headers_mut().as_deref_mut() - } } diff --git a/src/runtime/webcore/Response.rs b/src/runtime/webcore/Response.rs index 1a6df36bb3a0..e92bb2872555 100644 --- a/src/runtime/webcore/Response.rs +++ b/src/runtime/webcore/Response.rs @@ -1,6 +1,5 @@ use core::cell::Cell; use core::mem; -use core::ptr::NonNull; use bun_jsc::JsCell; @@ -17,7 +16,7 @@ use bun_http_types::Method::Method; use super::blob::Internal as InternalBlob; use super::body::{Body, BodyMixin, Value as BodyValue}; -use super::{FetchHeaders, ReadableStream, Request}; +use super::{ReadableStream, Request}; // Codegen (`generated_classes.rs`) re-exports `Blob` from // `crate::webcore::response` because the `.classes.ts` source path is @@ -38,96 +37,8 @@ unsafe extern "C" { ); } -/// RAII handle to a C++-owned `WebCore::FetchHeaders`. -/// -/// Holds exactly one ref on the C++ intrusive refcount; `Drop` releases it via -/// `WebCore__FetchHeaders__deref`. NOT a `std::rc::Rc` (the payload lives on -/// the C++ heap and is opaque here). -/// -/// Intentionally not `Clone`: the only "share" operation the surface -/// exposes is `clone_this()`, which deep-copies a fresh `FetchHeaders` on the -/// C++ side. Transferring ownership is by-move. -#[repr(transparent)] -pub struct HeadersRef(NonNull); - -impl HeadersRef { - /// Adopt a freshly-created `FetchHeaders*` (refcount already 1). - /// - /// # Safety - /// `ptr` must be a valid `WebCore::FetchHeaders*` and the caller must - /// transfer ownership of one ref. - #[inline] - pub(crate) unsafe fn adopt(ptr: NonNull) -> Self { - Self(ptr) - } - - #[inline] - pub(crate) fn as_ptr(&self) -> *mut FetchHeaders { - self.0.as_ptr() - } - - /// `FetchHeaders.createEmpty()` — fresh C++ allocation, refcount 1. - #[inline] - pub(crate) fn create_empty() -> Self { - // SAFETY: C++ allocates a new FetchHeaders with refcount 1; never null. - unsafe { Self::adopt(FetchHeaders::create_empty()) } - } - - /// `FetchHeaders.createFromUWS(req)` — fresh C++ allocation, refcount 1. - #[inline] - pub(crate) fn create_from_uws(uws_request: *mut core::ffi::c_void) -> Self { - // SAFETY: C++ allocates a new FetchHeaders with refcount 1; never null. - unsafe { Self::adopt(FetchHeaders::create_from_uws(uws_request)) } - } - - /// `FetchHeaders.createFromJS(global, value)` — may throw, may return null. - #[inline] - pub(crate) fn create_from_js( - global: &JSGlobalObject, - value: JSValue, - ) -> JsResult> { - // SAFETY: C++ returns a +1 ref or null. - Ok(FetchHeaders::create_from_js(global, value)?.map(|p| unsafe { Self::adopt(p) })) - } - - /// `FetchHeaders.cloneThis(global)` — deep copy on the C++ side. - #[inline] - pub(crate) fn clone_this(&self, global: &JSGlobalObject) -> JsResult> { - // SAFETY: C++ returns a +1 ref or null. - Ok(bun_opaque::opaque_deref_mut(self.0.as_ptr()) - .clone_this(global)? - .map(|p| unsafe { Self::adopt(p) })) - } -} - -impl core::ops::Deref for HeadersRef { - type Target = FetchHeaders; - #[inline] - fn deref(&self) -> &FetchHeaders { - // `FetchHeaders` is an opaque ZST FFI handle (S008); `self.0` is live - // for the lifetime of `self` — safe `*const → &` via `opaque_deref`. - bun_opaque::opaque_deref(self.0.as_ptr()) - } -} - -impl core::ops::DerefMut for HeadersRef { - #[inline] - fn deref_mut(&mut self) -> &mut FetchHeaders { - // `FetchHeaders` is an opaque ZST FFI handle (S008); `self.0` is live - // for the lifetime of `self` — safe `*mut → &mut` via `opaque_deref_mut`. - bun_opaque::opaque_deref_mut(self.0.as_ptr()) - } -} - -impl Drop for HeadersRef { - #[inline] - fn drop(&mut self) { - // `self.0` is live; releasing our +1 ref via WebCore__FetchHeaders__deref. - // Explicit UFCS to avoid `core::ops::Deref::deref` resolution ambiguity. - // `FetchHeaders` is an opaque ZST FFI handle (S008) — safe deref. - FetchHeaders::deref(bun_opaque::opaque_deref_mut(self.0.as_ptr())); - } -} +// Re-exported so that `crate::webcore::response::FetchHeaders` keeps resolving. +pub use bun_jsc::FetchHeaders; // `jsc.Codegen.JSResponse` — generated by `.classes.ts`. The Rust bindings // live in `bun_jsc::generated::JSResponse` (emitted by `js_class_module!`): @@ -191,7 +102,7 @@ pub struct Response { /// `handleResolveStream` / `handleRejectStream` can safely observe that the /// Response was GC'd (null) instead of dereferencing a freed pointer when /// backpressure lets GC run between `render()` and the async callback. - pub weak_ptr_data: WeakPtrData, + pub weak_ptr_data: Cell, js_ref: JsCell, // We must report a consistent value for this @@ -206,7 +117,7 @@ impl Default for Response { url: JsCell::new(OwnedString::new(BunString::empty())), redirected: Cell::new(false), ref_count: Cell::new(1), - weak_ptr_data: WeakPtrData::EMPTY, + weak_ptr_data: Cell::new(WeakPtrData::EMPTY), js_ref: JsCell::new(JsRef::empty()), reported_estimated_size: Cell::new(0), } @@ -214,9 +125,9 @@ impl Default for Response { } impl bun_ptr::weak_ptr::HasWeakPtrData for Response { - unsafe fn weak_ptr_data(this: *mut Self) -> *mut WeakPtrData { + unsafe fn weak_ptr_data(this: *mut Self) -> *const Cell { // SAFETY: caller guarantees `this` points to a live (possibly-finalized) allocation. - unsafe { core::ptr::addr_of_mut!((*this).weak_ptr_data) } + unsafe { core::ptr::addr_of!((*this).weak_ptr_data) } } } pub(crate) type WeakRef = bun_ptr::WeakPtr; @@ -257,15 +168,8 @@ impl BodyMixin for Response { Response::get_body_value(self) } #[inline] - fn get_fetch_headers(&self) -> Option> { - // Opaque C++ handle. Return the raw `*mut` - // directly (via `HeadersRef::as_ptr`) so the provenance is mutable; - // going through `as_deref()` would derive it from a `&FetchHeaders` - // and make the later `as_mut()` UB under Stacked Borrows. - self.init.get().headers.as_ref().map(|h| { - core::ptr::NonNull::new(h.as_ptr()) - .expect("HeadersRef wraps a non-null *mut FetchHeaders") - }) + fn get_fetch_headers(&self) -> Option<&FetchHeaders> { + self.init.get().headers.as_ref() } #[inline] fn get_form_data_encoding( @@ -299,8 +203,8 @@ impl Response { } #[inline] - pub fn set_init_headers(&self, headers: Option) { - // old headers dropped (HeadersRef::Drop derefs the C++ handle) + pub fn set_init_headers(&self, headers: Option) { + // old headers dropped (FetchHeaders::Drop derefs the C++ handle) self.init.with_mut(|init| init.headers = headers); } @@ -340,36 +244,15 @@ impl Response { self.url.get().get() } + /// Shared accessor over the C++ handle; mirrored by `Request::headers`. + /// Every `FetchHeaders` method takes `&self`, so this reaches all of them. #[inline] - pub fn get_init_headers(&self) -> Option<&FetchHeaders> { - self.init.get().headers.as_deref() - } - - /// R-2 `JsCell` escape hatch — single-JS-thread invariant. Centralises the - /// `unsafe { self.init.get_mut() }` deref so the four call sites - /// ([`get_init_headers_mut`], [`header`], [`get_or_create_headers`], - /// [`get_content_type`]) read it as a plain `&mut Init`. - /// - /// # Safety (encapsulated) - /// `Response` is JS-thread-affine (`!Sync`) and `init` is never reborrowed - /// across re-entrant JS; the returned `&mut Init` is held only for FFI - /// out-param writes (`FetchHeaders::fast_get`/`put`) that do not call back - /// into Response host-fns, so no overlapping `&mut Init` is live. - #[inline] - #[allow(clippy::mut_from_ref)] - fn init_mut(&self) -> &mut Init { - // SAFETY: see fn doc — single-JS-thread, no overlapping `&mut Init`. - unsafe { self.init.get_mut() } - } - - #[inline] - #[allow(clippy::mut_from_ref)] - pub fn get_init_headers_mut(&self) -> Option<&mut FetchHeaders> { - self.init_mut().headers.as_deref_mut() + pub fn headers(&self) -> Option<&FetchHeaders> { + self.init.get().headers.as_ref() } #[inline] - pub fn swap_init_headers(&self) -> Option { + pub fn swap_init_headers(&self) -> Option { self.init.with_mut(|init| init.headers.take()) } @@ -554,10 +437,6 @@ mod _jsc_host_fns { } // mod _jsc_host_fns impl Response { - pub fn get_fetch_headers(&self) -> Option<&FetchHeaders> { - self.init.get().headers.as_deref() - } - #[inline] pub fn status_code(&self) -> u16 { self.init.get().status_code @@ -571,11 +450,9 @@ impl Response { } pub fn header(&self, name: HTTPHeaderName) -> Option { - // reshaped for borrowck — `FetchHeaders::fast_get` takes - // `&mut self` (FFI writes through an out-param), so we return the - // owned `ZigString` instead of a borrowed slice. Callers do - // `.slice()` themselves. R-2 escape hatch via `init_mut()`. - self.init_mut().headers.as_mut()?.fast_get(name) + // `fast_get` writes through an out-param, so we return the owned + // `ZigString` instead of a borrowed slice. Callers do `.slice()`. + self.headers()?.fast_get(name) } pub fn is_ok(&self) -> bool { @@ -617,22 +494,18 @@ impl Response { JSValue::js_number(this.init.get().status_code as f64) } - #[allow(clippy::mut_from_ref)] pub(crate) fn get_or_create_headers( &self, global_this: &JSGlobalObject, - ) -> JsResult<&mut HeadersRef> { - // R-2 escape hatch via `init_mut()` — the returned `&mut HeadersRef` - // borrows `self.init`; callers (`get_headers`, `construct_*`) do not - // hold the borrow across calls that re-enter Response host-fns. - let init = self.init_mut(); - if init.headers.is_none() { - init.headers = Some(HeadersRef::create_empty()); + ) -> JsResult<&FetchHeaders> { + if self.init.get().headers.is_none() { + self.init + .with_mut(|init| init.headers = Some(FetchHeaders::create_empty())); if let BodyValue::Blob(blob) = self.body.get().value.get() { let content_type = blob.content_type_slice(); if !content_type.is_empty() { - init.headers.as_mut().unwrap().put( + self.headers().unwrap().put( HTTPHeaderName::ContentType, &BunString::ascii(content_type), global_this, @@ -641,7 +514,7 @@ impl Response { } } - Ok(init.headers.as_mut().unwrap()) + Ok(self.headers().unwrap()) } pub fn get_headers(this: &Self, global_this: &JSGlobalObject) -> JsResult { @@ -649,9 +522,7 @@ impl Response { } pub fn get_content_type(&self) -> JsResult> { - // R-2 escape hatch via `init_mut()` — `fast_get` (FFI out-param write) - // does not re-enter JS. - if let Some(headers) = self.init_mut().headers.as_mut() { + if let Some(headers) = self.headers() { if let Some(value) = headers.fast_get(HTTPHeaderName::ContentType) { return Ok(Some(value.to_slice())); } @@ -833,7 +704,7 @@ impl Response { // `?` below releases the cloned body payload. let body = scopeguard::guard(body, |b| b.reset()); let init = self.init.get().clone(global_this)?; - // Init's drop glue (HeadersRef + OwnedString) + // Init's drop glue (FetchHeaders + OwnedString) // handles cleanup on `?` below Ok(Response { body: JsCell::new(scopeguard::ScopeGuard::into_inner(body)), @@ -858,7 +729,7 @@ impl Response { // returns false and the allocation outlives this call until the // last WeakRef releases it. // - // - `Init` field drop glue releases `headers` (HeadersRef::Drop → + // - `Init` field drop glue releases `headers` (FetchHeaders::Drop → // C++ deref) and `status_text` (OwnedString::Drop → WTF deref). // - `Body` has NO `Drop`; `reset()` is the explicit cleanup API // (Body.rs renames `deinit` → `reset`). `drop_in_place` here @@ -873,7 +744,10 @@ impl Response { // Contents are gone; the allocation itself stays until any outstanding // WeakRef derefs (RequestContext.response_weakref). WeakRef.get() returns // null from here on. - if (*this).weak_ptr_data.on_finalize() { + let mut weak_data = (*this).weak_ptr_data.get(); + let last_ref = weak_data.on_finalize(); + (*this).weak_ptr_data.set(weak_data); + if last_ref { // Do NOT use heap::take — that would re-run field drop glue // on init/url/js_ref. They are now safe-empty so the second drop // would be a no-op, but it is still wasted work and fragile under @@ -933,7 +807,7 @@ impl Response { let mut args = bun_jsc::ArgumentsSlice::init(global_this.bun_vm(), &args_list.ptr[0..args_list.len]); - // `Init`'s field drop glue (HeadersRef + OwnedString) + // `Init`'s field drop glue (FetchHeaders + OwnedString) // releases its refs on `?`. `Body` has NO `Drop` and its // `WTFStringImpl` arm is a raw `*mut` (no drop glue), so wrap the // stack value in a scopeguard that calls `body.reset()` @@ -1096,7 +970,7 @@ impl Response { } else { url_string_value.to_bun_string(global_this)? }); - // `Init`'s drop glue (HeadersRef + OwnedString) + // `Init`'s drop glue (FetchHeaders + OwnedString) // handles cleanup on `?`. if let Some(arg_init) = args.next_eat() { @@ -1227,7 +1101,7 @@ impl Response { } } } - let mut init: Init = 'brk: { + let init: Init = 'brk: { if arguments[1].is_undefined_or_null() { break 'brk Init { status_code: 200, @@ -1245,7 +1119,7 @@ impl Response { } return Err(bun_jsc::JsError::Thrown); }; - // Init's field drop glue (HeadersRef + OwnedString) + // Init's field drop glue (FetchHeaders + OwnedString) // handles cleanup on `?` below if global_this.has_exception() { @@ -1271,7 +1145,7 @@ impl Response { // doing it on stack locals lets `?` trigger the scopeguard and // `init`'s drop glue and avoids leaking the heap allocation entirely. if let BodyValue::Blob(blob) = body.value.get() { - if let Some(headers) = init.headers.as_deref_mut() { + if let Some(headers) = init.headers.as_ref() { let content_type = blob.content_type_slice(); if !content_type.is_empty() && !headers.fast_has(HTTPHeaderName::ContentType) { headers.put( @@ -1304,7 +1178,7 @@ impl Response { } } -// `headers: Option` +// `headers: Option` // has `Drop` (releases the C++ ref) and `status_text: OwnedString` has `Drop` // (releases the WTF ref) — `BunString` itself is `Copy` and has NO `Drop`, so // the field MUST be `OwnedString` for auto-generated drop glue to perform the @@ -1313,7 +1187,7 @@ impl Response { // (e.g. Request::construct_into reading `response_init.headers`) keep working; // the remaining `status_text` is still dropped at scope end via field drop glue. pub struct Init { - pub headers: Option, + pub headers: Option, pub status_code: u16, pub status_text: OwnedString, pub method: Method, @@ -1335,7 +1209,7 @@ impl Init { let headers = match &self.headers { // `clone_this` does a deep copy on the C++ side and may return // null on OOM/throw. Flatten the - // `Option` so a null clone leaves `headers` empty. + // `Option` so a null clone leaves `headers` empty. Some(head) => head.clone_this(ctx)?, None => None, }; @@ -1352,7 +1226,7 @@ impl Init { status_code: 200, ..Default::default() }; - // Init's drop glue on `result` (HeadersRef + OwnedString) + // Init's drop glue on `result` (FetchHeaders + OwnedString) // handles cleanup on `?` below if !response_init.is_cell() { @@ -1372,7 +1246,7 @@ impl Init { // SAFETY: `as_direct` returned a live `*mut Request` owned by the // JS wrapper cell; the wrapper is rooted by `response_init` for // the duration of this call, so no GC can finalize it here. - let req = unsafe { &mut *req }; + let req = unsafe { &*req }; if let Some(headers) = req.get_fetch_headers_unless_empty() { result.headers = headers.clone_this(global_this)?; } @@ -1398,18 +1272,12 @@ impl Init { // FetchHeaders is a hand-bound opaque, so use its dedicated // `cast()` (wraps `WebCore__FetchHeaders__cast_`). if let Some(orig) = FetchHeaders::cast(headers) { - // `orig` is a live `WebCore::FetchHeaders*` borrowed from JS; - // `FetchHeaders` is an opaque ZST FFI handle (S008) — safe deref. - let orig = bun_opaque::opaque_deref_mut(orig.as_ptr()); + // Borrowed from the JS `Headers` wrapper (+0); `clone_this` is the +1. if !orig.is_empty() { - result.headers = orig.clone_this(global_this)?.map(|p| { - // SAFETY: `clone_this` returns a fresh +1-ref'd `FetchHeaders*`; - // ownership of that ref is transferred into the `HeadersRef`. - unsafe { HeadersRef::adopt(p) } - }); + result.headers = orig.clone_this(global_this)?; } } else { - result.headers = HeadersRef::create_from_js(global_this, headers)?; + result.headers = FetchHeaders::create_from_js(global_this, headers)?; } } diff --git a/src/runtime/webcore/S3Client.rs b/src/runtime/webcore/S3Client.rs index 0e6cba37d083..cdced396db86 100644 --- a/src/runtime/webcore/S3Client.rs +++ b/src/runtime/webcore/S3Client.rs @@ -360,27 +360,20 @@ impl S3Client { } }; let options = args.next_eat(); - // `Blob::new` heap-promotes and marks `ref_count = 1` so - // the JSS3File wrapper's `finalize` knows to free the blob. - let blob = crate::webcore::blob::Blob::new( - S3File::construct_s3_file_with_s3_credentials_and_options( - global, - path, - options, - &ptr.credentials, - ptr.options, - ptr.acl, - ptr.storage_class, - ptr.request_payer, - )?, - ); - // `to_js` runs `calculateEstimatedByteSize()` - // before wrapping the heap Blob in a JSS3File so JSC sees the correct - // GC pressure. Route through `BlobExt::to_js` (the `&mut self` method - // that owns the heap pointer), same as `S3File::construct_internal_js`. - // SAFETY: `blob` is a freshly leaked `*mut Blob` from `Blob::new`; - // `to_js` hands ownership of that pointer to the C++ wrapper. - Ok(unsafe { &mut *blob }.to_js(global)) + let blob = Box::new(S3File::construct_s3_file_with_s3_credentials_and_options( + global, + path, + options, + &ptr.credentials, + ptr.options, + ptr.acl, + ptr.storage_class, + ptr.request_payer, + )?); + // `into_js` runs `calculateEstimatedByteSize()` before wrapping the heap + // Blob in a JSS3File so JSC sees the correct GC pressure, then hands the + // allocation to the C++ wrapper. + Ok(blob.into_js(global)) } #[bun_jsc::host_fn(method)] diff --git a/src/runtime/webcore/S3File.rs b/src/runtime/webcore/S3File.rs index a9a0c929b03e..311de59c9526 100644 --- a/src/runtime/webcore/S3File.rs +++ b/src/runtime/webcore/S3File.rs @@ -462,8 +462,8 @@ fn construct_s3_file_internal( global: &JSGlobalObject, path: PathLike, options: Option, -) -> JsResult<*mut Blob> { - Ok(Blob::new(construct_s3_file_internal_store( +) -> JsResult> { + Ok(Blob::new_boxed(construct_s3_file_internal_store( global, path, options, )?)) } @@ -477,10 +477,6 @@ pub(crate) struct S3BlobStatTask { } impl S3BlobStatTask { - pub(crate) fn new(init: S3BlobStatTask) -> *mut S3BlobStatTask { - bun_core::heap::into_raw(Box::new(init)) - } - pub(crate) fn on_s3_exists_resolved( result: s3::S3StatResult, this: *mut core::ffi::c_void, @@ -581,85 +577,51 @@ impl S3BlobStatTask { Ok(()) } - pub(crate) fn exists(global: &JSGlobalObject, blob: &Blob) -> JsResult { - let this = S3BlobStatTask::new(S3BlobStatTask { + /// Ownership of the task transfers to `s3::stat`, which invokes `on_resolved` + /// exactly once on every path; the callback reclaims the box via `heap::take`. + fn spawn( + global: &JSGlobalObject, + blob: &Blob, + on_resolved: fn( + s3::S3StatResult, + *mut core::ffi::c_void, + ) -> Result<(), bun_jsc::JsTerminated>, + ) -> JsResult { + let task = Box::new(S3BlobStatTask { promise: bun_jsc::JSPromiseStrong::init(global), store: blob.store.get().as_ref().unwrap().clone(), global: bun_ptr::BackRef::new(global), }); - // SAFETY: `this` is a freshly leaked Box; valid for the duration of this call - let this_ref = unsafe { &mut *this }; - let promise = this_ref.promise.value(); + let promise = task.promise.value(); let s3_store = blob.store.get().as_ref().unwrap().data.as_s3(); let credentials = s3_store.get_credentials(); let path = s3_store.path(); // `Transpiler::env_mut` is the safe accessor for the process-singleton // dotenv loader (set during init). let env = global.bun_vm().as_mut().transpiler.env_mut(); + let ctx = bun_core::heap::into_raw(task).cast::(); s3::stat( credentials, path, - S3BlobStatTask::on_s3_exists_resolved, - this.cast::(), + on_resolved, + ctx, env.get_http_proxy(true, None, None).map(|proxy| proxy.href), s3_store.request_payer, )?; Ok(promise) } - pub(crate) fn stat(global: &JSGlobalObject, blob: &Blob) -> JsResult { - let this = S3BlobStatTask::new(S3BlobStatTask { - promise: bun_jsc::JSPromiseStrong::init(global), - store: blob.store.get().as_ref().unwrap().clone(), - global: bun_ptr::BackRef::new(global), - }); - // SAFETY: `this` is a freshly leaked Box; valid for the duration of this call - let this_ref = unsafe { &mut *this }; - let promise = this_ref.promise.value(); - let s3_store = blob.store.get().as_ref().unwrap().data.as_s3(); - let credentials = s3_store.get_credentials(); - let path = s3_store.path(); - // `Transpiler::env_mut` is the safe accessor for the process-singleton - // dotenv loader (set during init). - let env = global.bun_vm().as_mut().transpiler.env_mut(); + pub(crate) fn exists(global: &JSGlobalObject, blob: &Blob) -> JsResult { + Self::spawn(global, blob, S3BlobStatTask::on_s3_exists_resolved) + } - s3::stat( - credentials, - path, - S3BlobStatTask::on_s3_stat_resolved, - this.cast::(), - env.get_http_proxy(true, None, None).map(|proxy| proxy.href), - s3_store.request_payer, - )?; - Ok(promise) + pub(crate) fn stat(global: &JSGlobalObject, blob: &Blob) -> JsResult { + Self::spawn(global, blob, S3BlobStatTask::on_s3_stat_resolved) } pub(crate) fn size(global: &JSGlobalObject, blob: &mut Blob) -> JsResult { - let this = S3BlobStatTask::new(S3BlobStatTask { - promise: bun_jsc::JSPromiseStrong::init(global), - store: blob.store.get().as_ref().unwrap().clone(), - global: bun_ptr::BackRef::new(global), - }); - // SAFETY: `this` is a freshly leaked Box; valid for the duration of this call - let this_ref = unsafe { &mut *this }; - let promise = this_ref.promise.value(); - let s3_store = blob.store.get().as_ref().unwrap().data.as_s3(); - let credentials = s3_store.get_credentials(); - let path = s3_store.path(); - // `Transpiler::env_mut` is the safe accessor for the process-singleton - // dotenv loader (set during init). - let env = global.bun_vm().as_mut().transpiler.env_mut(); - - s3::stat( - credentials, - path, - S3BlobStatTask::on_s3_size_resolved, - this.cast::(), - env.get_http_proxy(true, None, None).map(|proxy| proxy.href), - s3_store.request_payer, - )?; - Ok(promise) + Self::spawn(global, blob, S3BlobStatTask::on_s3_size_resolved) } // Teardown (store deref, promise deinit, freeing the box) is handled by Box Drop. @@ -855,11 +817,9 @@ pub(crate) fn construct_internal_js( options: Option, ) -> JsResult { let blob = construct_s3_file_internal(global, path, options)?; - // SAFETY: `blob` is a freshly heap-allocated `*mut Blob` from `Blob::new`. - // Call the `BlobExt::to_js` `&mut self` method (not the by-value - // `JsClass::to_js`), which hands the existing heap pointer to the C++ - // wrapper. - Ok(BlobExt::to_js(unsafe { &mut *blob }, global)) + // `BlobExt::to_js` (not the by-value `JsClass::to_js`) hands the existing + // heap pointer to the C++ wrapper, which adopts it. + Ok(BlobExt::to_js(&*bun_core::heap::release(blob), global)) } pub fn to_js_unchecked(global: &JSGlobalObject, this: *mut Blob) -> JSValue { @@ -880,7 +840,11 @@ pub(crate) fn construct_internal( let Some(path) = PathLike::from_js(global, &mut args)? else { return Err(global.throw_invalid_arguments(format_args!("Expected file path string"))); }; - construct_s3_file_internal(global, path, args.next_eat()) + Ok(bun_core::heap::into_raw(construct_s3_file_internal( + global, + path, + args.next_eat(), + )?)) } // Hand-written ABI shim: returns `*mut Blob` (codegen constructor contract), diff --git a/src/runtime/webcore/Sink.rs b/src/runtime/webcore/Sink.rs index a6edf0c55f03..8bab766905b9 100644 --- a/src/runtime/webcore/Sink.rs +++ b/src/runtime/webcore/Sink.rs @@ -1,4 +1,6 @@ use core::ffi::c_void; +use core::marker::PhantomData; +use core::ptr::NonNull; use crate::api::bun_subprocess::Subprocess; use crate::webcore::streams::{self, Signal}; @@ -30,37 +32,29 @@ pub use crate::webcore::file_sink::FileSink; /// A `Sink` is a hand-rolled vtable-based writable stream sink. pub struct Sink<'a> { - // LIFETIMES.tsv: BORROW_PARAM — init_with_type stores the handler borrow; - // no deinit, end() only dispatches - pub ptr: &'a mut (), + // LIFETIMES.tsv: BORROW_PARAM — `init_with_type` erases the handler to a + // non-null ctx pointer; the vtable thunks re-type it. No borrow is held + // across dispatch, which reaches JS. + pub ptr: NonNull<()>, pub vtable: VTable, pub status: Status, pub used: bool, + _handler: PhantomData<&'a mut ()>, } impl<'a> Sink<'a> { - // `ptr` stays `&'a mut ()`: a reference to a - // zero-sized type only needs a non-null, aligned address, so a dangling - // pointer is a *valid* `&mut ()` (the same rule `Box<()>` relies on). pub fn pending() -> Sink<'static> { - // SAFETY: `()` is zero-sized, so `NonNull::dangling()` (non-null, - // aligned) is valid to reborrow as `&mut ()`; nothing is ever read or - // written through it. status == Closed gates all dispatch so neither - // `ptr` nor `vtable` is used before being overwritten by init_with_type. - // - // Both `zeroed()` and - // `MaybeUninit::uninit().assume_init()` are immediate UB for a struct of - // non-nullable `fn` pointers (niche-bearing). Instead we install a *valid* - // sentinel vtable whose entries unconditionally panic — this keeps the value - // well-formed at all times and turns any accidental dispatch - // into a loud, deterministic crash. - unsafe { - Sink { - ptr: &mut *core::ptr::NonNull::<()>::dangling().as_ptr(), - vtable: VTable::PENDING, - status: Status::Closed, - used: false, - } + // Both `zeroed()` and `MaybeUninit::uninit().assume_init()` are immediate UB + // for a struct of non-nullable `fn` pointers (niche-bearing). Instead install a + // *valid* sentinel vtable whose entries unconditionally panic: `status == + // Closed` gates all dispatch, so any accidental call is a deterministic crash + // rather than a wild jump. `ptr` is never read before `init_with_type`. + Sink { + ptr: NonNull::dangling(), + vtable: VTable::PENDING, + status: Status::Closed, + used: false, + _handler: PhantomData, } } } @@ -151,11 +145,12 @@ macro_rules! impl_sink_handler { pub fn init_with_type(handler: &mut T) -> Sink<'_> { Sink { - // SAFETY: type-erased borrow; recovered as *mut T in vtable thunks below. - ptr: unsafe { &mut *std::ptr::from_mut::(handler).cast::<()>() }, + // Type-erased ctx pointer; re-typed as `T` by the vtable thunks below. + ptr: NonNull::from(handler).cast::<()>(), vtable: VTable::wrap::(), status: Status::Ready, used: false, + _handler: PhantomData, } } @@ -292,11 +287,11 @@ impl UTF8Fallback { } } -pub type WriteUtf16Fn = fn(*mut (), &streams::Result) -> streams::result::Writable; -pub type WriteUtf8Fn = fn(*mut (), &streams::Result) -> streams::result::Writable; -pub type WriteLatin1Fn = fn(*mut (), &streams::Result) -> streams::result::Writable; -pub type EndFn = fn(*mut (), Option) -> sys::Result<()>; -pub type ConnectFn = fn(*mut (), Signal) -> sys::Result<()>; +pub type WriteUtf16Fn = fn(NonNull<()>, &streams::Result) -> streams::result::Writable; +pub type WriteUtf8Fn = fn(NonNull<()>, &streams::Result) -> streams::result::Writable; +pub type WriteLatin1Fn = fn(NonNull<()>, &streams::Result) -> streams::result::Writable; +pub type EndFn = fn(NonNull<()>, Option) -> sys::Result<()>; +pub type ConnectFn = fn(NonNull<()>, Signal) -> sys::Result<()>; #[derive(Clone, Copy)] pub struct VTable { @@ -318,15 +313,15 @@ impl VTable { /// if that invariant is ever violated we get a deterministic panic instead of a wild jump. pub const PENDING: VTable = { #[cold] - fn trap_write(_: *mut (), _: &streams::Result) -> streams::result::Writable { + fn trap_write(_: NonNull<()>, _: &streams::Result) -> streams::result::Writable { unreachable!("Sink vtable called while pending (status == Closed)") } #[cold] - fn trap_end(_: *mut (), _: Option) -> sys::Result<()> { + fn trap_end(_: NonNull<()>, _: Option) -> sys::Result<()> { unreachable!("Sink vtable called while pending (status == Closed)") } #[cold] - fn trap_connect(_: *mut (), _: Signal) -> sys::Result<()> { + fn trap_connect(_: NonNull<()>, _: Signal) -> sys::Result<()> { unreachable!("Sink vtable called while pending (status == Closed)") } VTable { @@ -340,33 +335,35 @@ impl VTable { pub fn wrap() -> VTable { fn on_write( - this: *mut (), + this: NonNull<()>, data: &streams::Result, ) -> streams::result::Writable { - // SAFETY: `this` was erased from `&mut W` in init_with_type. - unsafe { &mut *this.cast::() }.write(data) + // SAFETY: `this` is the `NonNull` erased by init_with_type. The `&mut W` + // is the receiver of one safe method and dies at return; nothing else holds + // a borrow of the handler while `write` reaches JS. + unsafe { &mut *this.cast::().as_ptr() }.write(data) } - fn on_connect(this: *mut (), signal: Signal) -> sys::Result<()> { + fn on_connect(this: NonNull<()>, signal: Signal) -> sys::Result<()> { // SAFETY: see on_write - unsafe { &mut *this.cast::() }.connect(signal) + unsafe { &mut *this.cast::().as_ptr() }.connect(signal) } fn on_write_latin1( - this: *mut (), + this: NonNull<()>, data: &streams::Result, ) -> streams::result::Writable { // SAFETY: see on_write - unsafe { &mut *this.cast::() }.write_latin1(data) + unsafe { &mut *this.cast::().as_ptr() }.write_latin1(data) } fn on_write_utf16( - this: *mut (), + this: NonNull<()>, data: &streams::Result, ) -> streams::result::Writable { // SAFETY: see on_write - unsafe { &mut *this.cast::() }.write_utf16(data) + unsafe { &mut *this.cast::().as_ptr() }.write_utf16(data) } - fn on_end(this: *mut (), err: Option) -> sys::Result<()> { + fn on_end(this: NonNull<()>, err: Option) -> sys::Result<()> { // SAFETY: see on_write - unsafe { &mut *this.cast::() }.end(err) + unsafe { &mut *this.cast::().as_ptr() }.end(err) } VTable { @@ -386,7 +383,7 @@ impl<'a> Sink<'a> { } self.status = Status::Closed; - (self.vtable.end)(std::ptr::from_mut::<()>(self.ptr), err) + (self.vtable.end)(self.ptr, err) } pub fn write_latin1(&mut self, data: &streams::Result) -> streams::result::Writable { @@ -394,7 +391,7 @@ impl<'a> Sink<'a> { return streams::result::Writable::Done; } - let res = (self.vtable.write_latin1)(std::ptr::from_mut::<()>(self.ptr), data); + let res = (self.vtable.write_latin1)(self.ptr, data); self.status = if res.is_done() || self.status == Status::Closed { Status::Closed } else { @@ -409,7 +406,7 @@ impl<'a> Sink<'a> { return streams::result::Writable::Done; } - let res = (self.vtable.write)(std::ptr::from_mut::<()>(self.ptr), data); + let res = (self.vtable.write)(self.ptr, data); self.status = if res.is_done() || self.status == Status::Closed { Status::Closed } else { @@ -424,7 +421,7 @@ impl<'a> Sink<'a> { return streams::result::Writable::Done; } - let res = (self.vtable.write_utf16)(std::ptr::from_mut::<()>(self.ptr), data); + let res = (self.vtable.write_utf16)(self.ptr, data); self.status = if res.is_done() || self.status == Status::Closed { Status::Closed } else { @@ -611,16 +608,15 @@ pub mod from_js_result { } impl JSSink { + /// Takes the canonical `*mut T`: the JS wrapper stashes the pointer and later + /// writes and frees through it, so no `&mut T` retag may narrow its + /// provenance to a borrow that the wrapper outlives. pub fn create_object( global: &crate::webcore::jsc::JSGlobalObject, - object: &mut T, + object: *mut T, destructor: usize, ) -> crate::webcore::jsc::JSValue { - T::create_object_extern( - global, - std::ptr::from_mut::(object).cast::(), - destructor, - ) + T::create_object_extern(global, object.cast::(), destructor) } pub fn set_destroy_callback(value: crate::webcore::jsc::JSValue, callback: usize) { @@ -637,18 +633,16 @@ impl JSSink { } } + /// Takes the canonical `*mut T` — same provenance rule as + /// [`create_object`](Self::create_object); the JS builtin re-enters the sink + /// through this pointer. pub fn assign_to_stream( global: &crate::webcore::jsc::JSGlobalObject, stream: crate::webcore::jsc::JSValue, - ptr: &mut T, + ptr: *mut T, jsvalue_ptr: *mut *mut c_void, ) -> crate::webcore::jsc::JSValue { - T::assign_to_stream_extern( - global, - stream, - std::ptr::from_mut::(ptr).cast::(), - jsvalue_ptr, - ) + T::assign_to_stream_extern(global, stream, ptr.cast::(), jsvalue_ptr) } /// `JSSink.detach(globalThis)` — disconnect the C++ controller cell stashed diff --git a/src/runtime/webcore/blob/Store.rs b/src/runtime/webcore/blob/Store.rs index 1594dd78e821..c60ec5f0fa4f 100644 --- a/src/runtime/webcore/blob/Store.rs +++ b/src/runtime/webcore/blob/Store.rs @@ -158,7 +158,7 @@ impl StoreExt for Store { mime_type, credentials, )), - mime_type: bun_http_types::MimeType::NONE, + mime_type: bun_jsc::JsCell::new(bun_http_types::MimeType::NONE), ref_count: bun_ptr::ThreadSafeRefCount::init(), is_all_ascii: None, })) @@ -179,7 +179,7 @@ impl StoreExt for Store { Ok(Store::new(Store { data: Data::S3(S3::init(path, mime_type, credentials)), - mime_type: bun_http_types::MimeType::NONE, + mime_type: bun_jsc::JsCell::new(bun_http_types::MimeType::NONE), ref_count: bun_ptr::ThreadSafeRefCount::init(), is_all_ascii: None, })) @@ -198,7 +198,7 @@ impl StoreExt for Store { Ok(Store::new(Store { data: Data::File(File::init(pathlike, mime_type)), - mime_type: bun_http_types::MimeType::NONE, + mime_type: bun_jsc::JsCell::new(bun_http_types::MimeType::NONE), ref_count: bun_ptr::ThreadSafeRefCount::init(), is_all_ascii: None, })) @@ -210,7 +210,7 @@ impl StoreExt for Store { fn init_mmap(slice: &'static mut [u8]) -> StoreRef { StoreRef::from(Store::new(Store { data: Data::Bytes(Bytes::init_mmap(slice)), - mime_type: bun_http_types::MimeType::NONE, + mime_type: bun_jsc::JsCell::new(bun_http_types::MimeType::NONE), ref_count: bun_ptr::ThreadSafeRefCount::init(), is_all_ascii: None, })) diff --git a/src/runtime/webcore/blob/copy_file.rs b/src/runtime/webcore/blob/copy_file.rs index 831c84a0bee4..1a20daea6a7b 100644 --- a/src/runtime/webcore/blob/copy_file.rs +++ b/src/runtime/webcore/blob/copy_file.rs @@ -1076,7 +1076,7 @@ impl<'a> CopyFileWindows<'a> { core::ptr::from_mut(&mut self.read_write_loop.uv_buf), 1, -1, - Some(on_read), + Some(on_fs::<{ FsOp::Read }>), ) }; @@ -1122,18 +1122,33 @@ impl ReadWriteLoop { } #[cfg(windows)] -extern "C" fn on_read(req: *mut libuv::fs_t) { - // SAFETY: `req->data` was set to `core::ptr::from_mut(self)` (whole-struct - // provenance) before scheduling. Recover the parent from `data` rather than - // `from_field_ptr!(.., io_request, req)`: the `req` pointer libuv hands back was - // produced from a `&mut self.io_request` reborrow whose provenance covers only the - // `io_request` field, so `container_of`-style subtraction would yield a - // `*mut CopyFileWindows` with out-of-bounds provenance (UB under Stacked/Tree - // Borrows). After forming `this`, access the request via `this.io_request` — never - // through `(*req)`, which would alias the live `&mut`. - let this: &mut CopyFileWindows = unsafe { &mut *(*req).data.cast::() }; +#[derive(ConstParamTy, PartialEq, Eq, Clone, Copy)] +enum FsOp { + Read, + Write, + CopyFile, + Chmod, +} + +/// The one place that recovers the parent from `req->data` (whole-struct provenance, +/// stored before scheduling). `from_field_ptr!` would be wrong: `req` carries only +/// `io_request`-field provenance. Reach the request via `this.io_request`, never `(*req)`. +#[cfg(windows)] +unsafe extern "C" fn on_fs(req: *mut libuv::fs_t) { + // SAFETY: libuv invokes this on the loop thread with `req->data` set to the live, + // exclusively-owned `*mut CopyFileWindows`. + let this: &mut CopyFileWindows = unsafe { bun_ptr::callback_ctx((*req).data) }; debug_assert!(core::ptr::addr_of_mut!(this.io_request) == req); + match OP { + FsOp::Read => on_read(this), + FsOp::Write => on_write(this), + FsOp::CopyFile => on_copy_file(this), + FsOp::Chmod => on_chmod(this), + } +} +#[cfg(windows)] +fn on_read(this: &mut CopyFileWindows) { let source_fd = this.read_write_loop.source_fd; let destination_fd = this.read_write_loop.destination_fd; // reshaped for borrowck — `read_buf.items` is `Vec` len-slice. @@ -1178,7 +1193,7 @@ extern "C" fn on_read(req: *mut libuv::fs_t) { core::ptr::from_mut(&mut this.read_write_loop.uv_buf), 1, -1, - Some(on_write), + Some(on_fs::<{ FsOp::Write }>), ) }; this.io_request.data = core::ptr::from_mut(this).cast::(); @@ -1191,11 +1206,7 @@ extern "C" fn on_read(req: *mut libuv::fs_t) { } #[cfg(windows)] -extern "C" fn on_write(req: *mut libuv::fs_t) { - // SAFETY: see `on_read` — recover from `req->data` (whole-struct provenance), - // not `from_field_ptr!`; then access the request only via `this.io_request`. - let this: &mut CopyFileWindows = unsafe { &mut *(*req).data.cast::() }; - debug_assert!(core::ptr::addr_of_mut!(this.io_request) == req); +fn on_write(this: &mut CopyFileWindows) { let buf_len = this.read_write_loop.read_buf.len(); let destination_fd = this.read_write_loop.destination_fd; @@ -1243,7 +1254,7 @@ extern "C" fn on_write(req: *mut libuv::fs_t) { core::ptr::from_mut(&mut this.read_write_loop.uv_buf), 1, -1, - Some(on_write), + Some(on_fs::<{ FsOp::Write }>), ) }; @@ -1294,7 +1305,7 @@ impl<'a> CopyFileWindows<'a> { ) -> JSValue { // destination_file_store.ref() / source_file_store.ref() — Arc clone let global = event_loop.global_ref(); - let result = bun_core::heap::into_raw(CopyFileWindows::new(CopyFileWindows { + let this = CopyFileWindows::new(CopyFileWindows { destination_file_store, source_file_store, promise: jsc::JSPromiseStrong::init(global), @@ -1307,14 +1318,15 @@ impl<'a> CopyFileWindows<'a> { written_bytes: 0, err: None, read_write_loop: ReadWriteLoop::default(), - })); - // SAFETY: result was just allocated above - let result_ref = unsafe { &mut *result }; - let promise = result_ref.promise.value(); + }); + let promise = this.promise.value(); + // Ownership moves to the libuv callbacks; `destroy(self: Box)` frees it. + let result = bun_core::heap::into_raw(this); // On error, this function might free the CopyFileWindows struct. // So we can no longer reference it beyond this point. - result_ref.copyfile(); + // SAFETY: `result` is the unique pointer just leaked from the box above. + unsafe { (*result).copyfile() }; promise } @@ -1535,7 +1547,7 @@ impl<'a> CopyFileWindows<'a> { old_path.as_ptr(), new_path.as_ptr(), 0, - Some(on_copy_file), + Some(on_fs::<{ FsOp::CopyFile }>), ) }; @@ -1569,8 +1581,9 @@ impl<'a> CopyFileWindows<'a> { let _guard = unsafe { jsc::event_loop::EventLoop::enter_scope(self.event_loop as *const _ as *mut _) }; - // SAFETY: self was heap-allocated in init(); destroy reclaims and drops it. self is not accessed afterward. - unsafe { Self::destroy(core::ptr::from_mut(self)) }; + // SAFETY: self was heap-allocated (Box) in init(); this is the unique owning + // pointer and self is not accessed afterward. + Self::destroy(unsafe { bun_core::heap::take(core::ptr::from_mut(self)) }); // `promise` points to a GC-owned `JSPromise` cell, not into `self`; valid after `destroy`. let _ = promise.reject(global_this, err_instance); // TODO: properly propagate exception upwards } @@ -1619,7 +1632,7 @@ impl<'a> CopyFileWindows<'a> { &mut self.io_request, path_ptr, i32::try_from(mode).expect("int cast"), - Some(on_chmod), + Some(on_fs::<{ FsOp::Chmod }>), ) }; @@ -1656,8 +1669,9 @@ impl<'a> CopyFileWindows<'a> { jsc::event_loop::EventLoop::enter_scope(self.event_loop as *const _ as *mut _) }; - // SAFETY: self was heap-allocated in init(); destroy reclaims and drops it. self is not accessed afterward. - unsafe { Self::destroy(core::ptr::from_mut(self)) }; + // SAFETY: self was heap-allocated (Box) in init(); this is the unique owning + // pointer and self is not accessed afterward. + Self::destroy(unsafe { bun_core::heap::take(core::ptr::from_mut(self)) }); // `promise` points to a GC-owned `JSPromise` cell, not into `self`; valid after `destroy`. let _ = promise.resolve(global_this, JSValue::js_number_from_uint64(written as u64)); // TODO: properly propagate exception upwards } @@ -1677,17 +1691,12 @@ impl<'a> CopyFileWindows<'a> { ); } - /// SAFETY: `this` must have been produced by `heap::alloc` in `init()` and - /// not yet destroyed. After this call `this` is dangling. - pub unsafe fn destroy(this: *mut Self) { - // SAFETY: caller contract — `this` is a live `heap::alloc`-ed pointer. - unsafe { - (*this).read_write_loop.close(); - // destination_file_store.deref() / source_file_store.deref() — Arc Drop on Box drop - // promise.deinit() — handled by JscStrong's Drop on Box drop - (*this).io_request.deinit(); - drop(bun_core::heap::take(this)); - } + /// Consumes the allocation made in `init()`. + /// `destination_file_store` / `source_file_store` (Arc deref) and `promise` + /// (JscStrong) are released by the derived `Drop` when the `Box` drops. + pub fn destroy(mut self: Box) { + self.read_write_loop.close(); + self.io_request.deinit(); } fn mkdirp(&mut self) { @@ -1740,12 +1749,7 @@ impl<'a> CopyFileWindows<'a> { } #[cfg(windows)] -extern "C" fn on_copy_file(req: *mut libuv::fs_t) { - // SAFETY: see `on_read` — recover from `req->data` (whole-struct provenance), - // not `from_field_ptr!`; then access the request only via `this.io_request`. - let this: &mut CopyFileWindows = unsafe { &mut *(*req).data.cast::() }; - debug_assert!(core::ptr::addr_of_mut!(this.io_request) == req); - +fn on_copy_file(this: &mut CopyFileWindows) { let event_loop = this.event_loop; event_loop.unref_concurrently(); let rc = this.io_request.result; @@ -1788,12 +1792,7 @@ extern "C" fn on_copy_file(req: *mut libuv::fs_t) { } #[cfg(windows)] -extern "C" fn on_chmod(req: *mut libuv::fs_t) { - // SAFETY: see `on_read` — recover from `req->data` (whole-struct provenance), - // not `from_field_ptr!`; then access the request only via `this.io_request`. - let this: &mut CopyFileWindows = unsafe { &mut *(*req).data.cast::() }; - debug_assert!(core::ptr::addr_of_mut!(this.io_request) == req); - +fn on_chmod(this: &mut CopyFileWindows) { let event_loop = this.event_loop; event_loop.unref_concurrently(); diff --git a/src/runtime/webcore/blob/read_file.rs b/src/runtime/webcore/blob/read_file.rs index a79cdc061347..00db3abc6cf2 100644 --- a/src/runtime/webcore/blob/read_file.rs +++ b/src/runtime/webcore/blob/read_file.rs @@ -23,6 +23,7 @@ use bun_jsc::event_loop::EventLoop; use bun_jsc::{ self as jsc, AnyPromise, JSGlobalObject, JSPromiseStrong, JSValue, JsResult, SystemError, }; +use bun_ptr::BackRef; #[cfg(windows)] use bun_sys::ReturnCodeExt as _; #[cfg(not(windows))] @@ -195,7 +196,7 @@ pub struct ReadFile { pub errno: Option, pub on_complete_ctx: *mut c_void, pub on_complete_callback: ReadFileOnReadFileCallback, - pub io_task: Option<*mut ReadFileTask>, + pub io_task: Option>, pub io_poll: io::Poll, pub io_request: io::Request, pub could_block: bool, @@ -620,11 +621,15 @@ impl ReadFile { Ok(()) } - pub fn run(&mut self, task: *mut ReadFileTask) { - self.run_async(task); + /// # Safety + /// `task` must be the live `ReadFileTask` that owns `self`; it is dereferenced + /// through `BackRef`. Called only from the work-pool trampoline. + pub unsafe fn run(&mut self, task: *mut ReadFileTask) { + // SAFETY: the WorkTask passes its own address and outlives `self`. + self.run_async(unsafe { BackRef::from_raw(task) }); } - fn run_async(&mut self, task: *mut ReadFileTask) { + fn run_async(&mut self, task: BackRef) { #[cfg(windows)] { let _ = task; @@ -659,10 +664,11 @@ impl ReadFile { } } if !close_after_io { - if let Some(io_task) = self.io_task.take() { + if let Some(mut io_task) = self.io_task.take() { bloblog!("ReadFile.onFinish() = immediately"); - // SAFETY: io_task is a non-null backref set in run(); WorkTask owns lifetime. - ReadFileTask::on_finish(unsafe { &mut *io_task }); + // SAFETY: taken out of the field, so no other borrow of the + // WorkTask is live for this call. + ReadFileTask::on_finish(unsafe { io_task.get_mut() }); } } } @@ -1094,27 +1100,25 @@ impl<'a> ReadFileUV<'a> { let _ = this_ptr; } - pub fn finalize(this: *mut Self) { + pub fn finalize(mut self: Box) { log!("ReadFileUV.finalize"); - // SAFETY: `this` was heap-allocated in start(); we reclaim ownership here. - let mut this_box = unsafe { bun_core::heap::take(this) }; - let event_loop = this_box.event_loop; + let event_loop = self.event_loop; - let cb = this_box.on_complete_fn; - let cb_ctx = this_box.on_complete_data; + let cb = self.on_complete_fn; + let cb_ctx = self.on_complete_data; - let result = if let Some(err) = this_box.system_error.take() { + let result = if let Some(err) = self.system_error.take() { ReadFileResultType::Err(err) } else { - // Move byte_store out so dropping `this_box` below does not free the + // Move byte_store out so dropping `self` below does not free the // buffer we hand to the callback. Normalize to `Box<[u8]>` so the // `is_temporary` consumer (Body.rs / Blob.rs) can soundly reclaim // via `heap::take` — handing out `(ptr, len)` from a ByteStore // whose `cap > len` would be a layout-mismatched dealloc. - let boxed = core::mem::take(&mut this_box.byte_store).into_boxed_slice(); + let boxed = core::mem::take(&mut self.byte_store).into_boxed_slice(); ReadFileResultType::Result(ReadFileRead { buf: bun_core::heap::into_raw(boxed), - total_size: this_box.total_size, + total_size: self.total_size, }) }; @@ -1123,8 +1127,8 @@ impl<'a> ReadFileUV<'a> { cb(cb_ctx, result); // store.deref runs via StoreRef's Drop when the Box drops. - this_box.req.deinit(); - drop(this_box); + self.req.deinit(); + drop(self); // Release the event loop reference now that we're done event_loop.unref_concurrently(); log!("ReadFileUV.finalize destroy"); @@ -1149,7 +1153,8 @@ impl<'a> ReadFileUV<'a> { } } - Self::finalize(core::ptr::from_mut(self)); + // SAFETY: `self` is the allocation from start(); the uv chain's last ref. + Self::finalize(unsafe { bun_core::heap::take(core::ptr::from_mut(self)) }); } pub fn on_file_open(&mut self, opened_fd: Fd) { diff --git a/src/runtime/webcore/blob/write_file.rs b/src/runtime/webcore/blob/write_file.rs index f49196680e5a..9633a247804b 100644 --- a/src/runtime/webcore/blob/write_file.rs +++ b/src/runtime/webcore/blob/write_file.rs @@ -1103,16 +1103,16 @@ mod windows_impl { // SAFETY: caller contract — `this` is live. if let Some(err) = unsafe { (*this).to_system_error() } { - // SAFETY: caller contract — `this` is live; consumed here. - unsafe { Self::deinit(this) }; + // SAFETY: caller contract — `this` is the unique live pointer, from `Self::new`. + unsafe { bun_core::heap::take(this) }.deinit(); if let Err(e) = cb(cb_ctx, WriteFileResultType::Err(Box::new(err))) { return e.into(); } } else { // SAFETY: caller contract — `this` is live. let wrote = unsafe { (*this).total_written }; - // SAFETY: caller contract — `this` is live; consumed here. - unsafe { Self::deinit(this) }; + // SAFETY: caller contract — `this` is the unique live pointer, from `Self::new`. + unsafe { bun_core::heap::take(this) }.deinit(); if let Err(e) = cb(cb_ctx, WriteFileResultType::Result(wrote as SizeType)) { return e.into(); } @@ -1231,31 +1231,17 @@ mod windows_impl { bun_core::heap::into_raw(Box::new(init)) } - /// # Safety - /// `this` must be the unique live pointer to a `WriteFileWindows` - /// allocated via [`Self::new`]. Consumes the allocation; `*this` is - /// freed and must not be accessed after this returns. - /// - /// Takes a raw pointer (not `&mut self`) because reclaiming the `Box` - /// while a `&mut self` argument is on the stack is a Stacked Borrows - /// protector violation (deallocating memory a protected reference - /// points into is UB even if the reference is never used again). - pub unsafe fn deinit(this: *mut Self) { - // SAFETY: caller contract — `this` is live. - unsafe { - let fd = (*this).fd; - if fd > 0 && (*this).owned_fd { - aio::Closer::close(Fd::from_uv(fd), (*this).io_request.loop_); - } - // The store derefs happen via `StoreRef::drop` when the Box is - // reclaimed below (paired with the RAII note in `create_with_ctx`). - (*this).poll_ref.disable(); - // (*this).io_request is a valid uv_fs_t embedded in this struct; uv_fs_req_cleanup - // is safe on a zeroed or previously-used req. - uv::uv_fs_req_cleanup(&mut (*this).io_request); - // `this` was allocated via Self::new (heap::into_raw); reclaim and drop here. - drop(bun_core::heap::take(this)); + /// Consumes the allocation produced by [`Self::new`]; freed when this returns. + pub fn deinit(mut self: Box) { + if self.fd > 0 && self.owned_fd { + aio::Closer::close(Fd::from_uv(self.fd), self.io_request.loop_); } + // The store derefs happen via `StoreRef::drop` when the Box drops + // below (paired with the RAII note in `create_with_ctx`). + self.poll_ref.disable(); + // SAFETY: `io_request` is a valid uv_fs_t embedded in this struct; uv_fs_req_cleanup + // is safe on a zeroed or previously-used req. + unsafe { uv::uv_fs_req_cleanup(&mut self.io_request) }; } pub fn create( diff --git a/src/runtime/webcore/fetch.rs b/src/runtime/webcore/fetch.rs index a98917610fd5..5bcb5a89773f 100644 --- a/src/runtime/webcore/fetch.rs +++ b/src/runtime/webcore/fetch.rs @@ -52,7 +52,7 @@ use bun_core::{String as BunString, Tag as BunStringTag, ZigStringSlice}; use bun_http::{self as http, FetchRedirect, Headers, HeadersExt as _, MimeType}; use bun_http_jsc::method_jsc; use bun_http_types::Method::Method; -use bun_jsc::{HTTPHeaderName, StringJsc as _, SysErrorJsc as _}; +use bun_jsc::{FetchHeaders, HTTPHeaderName, StringJsc as _, SysErrorJsc as _}; use bun_paths::{self, PathBuffer}; use bun_sys::FdExt as _; // `FromJsEnum for FetchRedirect` lives in bun_http_jsc; importing the impl crate @@ -66,7 +66,7 @@ use crate::webcore::body::{Action as BodyValueLockedAction, InternalBlob, Value use crate::webcore::headers_ref::any_blob_content_type_opt; use crate::webcore::s3::client as s3; use crate::webcore::{ - AbortSignal, Blob, Body, FetchHeaders, ObjectURLRegistry, ReadableStream, Request, Response, + AbortSignal, Blob, Body, ObjectURLRegistry, ReadableStream, Request, Response, }; use crate::webcore::{blob, readable_stream, response}; use bun_http_jsc as _; @@ -134,21 +134,6 @@ impl Drop for SignalRef { } } -/// RAII guard for the `+1` `FetchHeaders` ref returned by -/// `FetchHeaders::create_from_js`; releases the ref on every exit path of -/// `extract_headers`. -struct FetchHeadersRef(Option>); -impl Drop for FetchHeadersRef { - fn drop(&mut self) { - if let Some(fh) = self.0.take() { - // `fh` came from `FetchHeaders::create_from_js` which returns a - // +1-ref `NonNull`. `FetchHeaders` is an opaque ZST - // FFI handle (S008) — safe `*mut → &mut` via `opaque_deref_mut`. - bun_opaque::opaque_deref_mut(fh.as_ptr()).deref(); - } - } -} - /// `Blob.Any` accessor shim. trait AnyBlobExt { fn blob(&self) -> &Blob; @@ -493,24 +478,18 @@ fn fetch_impl( break 'brk None; }; - // kept as raw `*mut Request` because the body re-borrows it - // multiple times across long-lived option/init reads. - let request: Option<*mut Request> = 'brk: { + // Every accessor used below takes `&self` and mutates through `JsCell`, + // so a shared borrow suffices and may safely alias across JS re-entry. + let request: Option<&Request> = 'brk: { if first_arg.is_cell() { if let Some(request_) = first_arg.as_direct::() { - break 'brk Some(request_); + // SAFETY: `as_direct` yields the payload of a live JS-owned + // Request cell, pinned while `first_arg` is on the stack. + break 'brk Some(unsafe { &*request_ }); } } break 'brk None; }; - // Helper macro: short-lived `&mut Request` reborrow of the optional pointer. - macro_rules! request_mut { - () => { - // SAFETY: `request` was obtained from a live JS-owned Request via - // `as_direct`; each reborrow is non-overlapping at the call site. - request.map(|p| unsafe { &mut *p }) - }; - } // If it's NOT a Request or a subclass of Request, treat the first argument as a URL. let url_str_optional = if first_arg.as_::().is_none() { @@ -544,7 +523,7 @@ fn fetch_impl( break 'extract_url str; } - if let Some(req) = request_mut!() { + if let Some(req) = request { let _ = req.ensure_url(); // bun.handleOom — aborts on OOM break 'extract_url req.url.get().dupe_ref(); } @@ -636,7 +615,7 @@ fn fetch_impl( } } - if let Some(req) = request_mut!() { + if let Some(req) = request { break 'extract_method Some(req.method); } @@ -907,7 +886,7 @@ fn fetch_impl( // redirect: "follow" | "error" | "manual" | undefined; redirect_type = 'extract_redirect_type: { // First, try to use the Request object's redirect if available - if let Some(req) = request_mut!() { + if let Some(req) = request { redirect_type = req.flags.redirect; } @@ -1097,20 +1076,15 @@ fn fetch_impl( if !headers_value.is_undefined_or_null() { if let Some(fetch_hdrs) = FetchHeaders::cast(headers_value) { - // `cast` returns a live JS-owned FetchHeaders*; - // BackRef invariant holds for this read. - let fetch_hdrs = bun_ptr::BackRef::from(fetch_hdrs); + // `cast` borrows the JS-owned headers; no ref taken. proxy_headers = Some(from_fetch_headers(Some(&*fetch_hdrs), None)); } else if let Some(fetch_hdrs) = FetchHeaders::create_from_js(ctx, headers_value)? { - // `create_from_js` returns a +1-ref NonNull; - // RAII guard releases it on scope exit. - let _guard = FetchHeadersRef(Some(fetch_hdrs)); - let fetch_hdrs = bun_ptr::BackRef::from(fetch_hdrs); + // Owns the +1 from `create_from_js`; Drop releases it. proxy_headers = - Some(from_fetch_headers(Some(&*fetch_hdrs), None)); + Some(from_fetch_headers(Some(&fetch_hdrs), None)); } } } @@ -1166,7 +1140,7 @@ fn fetch_impl( } } - if let Some(req) = request_mut!() { + if let Some(req) = request { if let Some(signal_) = req.signal.get() { break 'extract_signal NonNull::new(signal_.ref_()); } @@ -1228,7 +1202,7 @@ fn fetch_impl( } } - if let Some(req) = request_mut!() { + if let Some(req) = request { let body_value = req.get_body_value(); let already_used = match body_value { BodyValue::Used => true, @@ -1304,25 +1278,26 @@ fn fetch_impl( headers = 'extract_headers: { // Releases the +1 from `create_from_js` on every exit path (including // the `has_exception()` early returns below). - let mut fetch_headers_to_deref = FetchHeadersRef(None); + let mut _fetch_headers_to_deref: Option = None; + // `cast` only borrows; this slot outlives the `if let` arm so the read below can point at it. + let mut borrowed_headers: Option> = None; - let fetch_headers: Option<*mut FetchHeaders> = 'brk: { + let fetch_headers: Option<&FetchHeaders> = 'brk: { if let Some(options) = options_object { if let Some(headers_value) = options.fast_get(global_this, jsc::BuiltinName::Headers)? { if !headers_value.is_undefined() { if let Some(headers__) = FetchHeaders::cast(headers_value) { - // `FetchHeaders` is an opaque ZST FFI handle (S008) — safe deref. - if bun_opaque::opaque_deref_mut(headers__.as_ptr()).is_empty() { + if headers__.is_empty() { break 'brk None; } - break 'brk Some(headers__.as_ptr()); + break 'brk Some(&**borrowed_headers.insert(headers__)); } if let Some(headers__) = FetchHeaders::create_from_js(ctx, headers_value)? { - fetch_headers_to_deref.0 = Some(headers__); - break 'brk Some(headers__.as_ptr()); + // Owns the +1 from `create_from_js`; Drop releases it. + break 'brk Some(_fetch_headers_to_deref.insert(headers__)); } break 'brk None; @@ -1334,9 +1309,9 @@ fn fetch_impl( } } - if let Some(req) = request_mut!() { + if let Some(req) = request { if let Some(head) = req.get_fetch_headers_unless_empty() { - break 'brk Some(head.as_ptr()); + break 'brk Some(head); } break 'brk None; } @@ -1347,16 +1322,15 @@ fn fetch_impl( { if !headers_value.is_undefined() { if let Some(headers__) = FetchHeaders::cast(headers_value) { - // `FetchHeaders` is an opaque ZST FFI handle (S008) — safe deref. - if bun_opaque::opaque_deref_mut(headers__.as_ptr()).is_empty() { + if headers__.is_empty() { break 'brk None; } - break 'brk Some(headers__.as_ptr()); + break 'brk Some(&**borrowed_headers.insert(headers__)); } if let Some(headers__) = FetchHeaders::create_from_js(ctx, headers_value)? { - fetch_headers_to_deref.0 = Some(headers__); - break 'brk Some(headers__.as_ptr()); + // Owns the +1 from `create_from_js`; Drop releases it. + break 'brk Some(_fetch_headers_to_deref.insert(headers__)); } break 'brk None; @@ -1375,11 +1349,7 @@ fn fetch_impl( return Ok(JSValue::ZERO); } - let result = if let Some(headers_) = fetch_headers { - // `headers_` points to a live FetchHeaders (either JS-owned or - // refcounted via `fetch_headers_to_deref` above). `FetchHeaders` is - // an opaque ZST FFI handle (S008) — safe `*mut → &mut` deref. - let headers_ref = bun_opaque::opaque_deref_mut(headers_); + let result = if let Some(headers_ref) = fetch_headers { if let Some(hostname_) = headers_ref.fast_get(HTTPHeaderName::Host) { hostname = Some(hostname_.to_owned_slice().into_boxed_slice()); } @@ -1406,7 +1376,7 @@ fn fetch_impl( headers }; - // `fetch_headers_to_deref` Drop releases the +1 from create_from_js. + // `_fetch_headers_to_deref` Drop releases the +1 from create_from_js. break 'extract_headers result; }; diff --git a/src/runtime/webcore/fetch/FetchTasklet.rs b/src/runtime/webcore/fetch/FetchTasklet.rs index c97257ce9993..dc7c93cf23c5 100644 --- a/src/runtime/webcore/fetch/FetchTasklet.rs +++ b/src/runtime/webcore/fetch/FetchTasklet.rs @@ -30,12 +30,10 @@ use crate::api::bun_x509 as X509; use crate::webcore::blob::{Any as AnyBlob, Blob, SizeType as BlobSizeType, Store as BlobStore}; use crate::webcore::body::{self, Body, Value as BodyValue, ValueError as BodyValueError}; use crate::webcore::readable_stream::{ReadableStream, Strong as ReadableStreamStrong}; -use crate::webcore::response::HeadersRef; +use crate::webcore::response::FetchHeaders; use crate::webcore::resumable_sink::ResumableFetchSink; use crate::webcore::streams::{StreamError, StreamResult}; -use crate::webcore::{ - AbortSignal, DrainResult, FetchHeaders, InternalBlob, Response, ResumableSinkBackpressure, -}; +use crate::webcore::{AbortSignal, DrainResult, InternalBlob, Response, ResumableSinkBackpressure}; use bun_jsc::JsTerminatedResult; // `bun_event_loop::JsResult` (cycle-broken erased error) — used by @@ -594,19 +592,18 @@ impl FetchTasklet { None } - /// `&mut`-yielding form of [`get_current_response`]. + /// Reference-yielding form of [`get_current_response`]. /// /// INVARIANT: when `Some`, the pointer is either `native_response` (one /// strong native ref held by the tasklet until `unref` in cleanup) or the /// `JSValue::as_::()` deref of a live JS handle pinned by - /// `self.response`. The `Response` is a separate JSC-cell allocation - /// disjoint from `FetchTasklet`, so the returned `&mut` does not overlap - /// any `&mut self` the caller may take afterwards (hence the unbounded - /// `'a`). JS-thread-only; no concurrent `&mut` exists. + /// `self.response`, so it outlives the `&self` borrow. `Response` mutates + /// through `JsCell`, so `&Response` suffices and stays valid across the + /// JS re-entry that `on_data` / `BodyValue::resolve` can trigger. #[inline] - fn current_response_mut<'a>(&self) -> Option<&'a mut Response> { + fn current_response(&self) -> Option<&Response> { // SAFETY: see INVARIANT above. - self.get_current_response().map(|r| unsafe { &mut *r }) + self.get_current_response().map(|r| unsafe { &*r }) } pub(crate) fn start_request_stream(&mut self) { @@ -686,7 +683,7 @@ impl FetchTasklet { js_err.ensure_still_alive(); } // if we are buffering resolve the promise - if let Some(response) = self.current_response_mut() { + if let Some(response) = self.current_response() { // body value now owns the error let err = scopeguard::ScopeGuard::into_inner(err); let body = response.get_body_value(); @@ -728,77 +725,78 @@ impl FetchTasklet { } } - if let Some(response) = self.current_response_mut() { - bun_output::scoped_log!(FetchTasklet, "onBodyReceived Current Response"); - let size_hint = self.get_size_hint(); - response.set_size_hint(size_hint); - if let Some(readable) = response.get_body_readable_stream(&global_this) { - bun_output::scoped_log!( - FetchTasklet, - "onBodyReceived CurrentResponse BodyReadableStream" - ); - if let Some(bytes) = readable.ptr.bytes() { - let chunk = self.scheduled_response_buffer.list.as_slice(); - - if self.result.has_more { - bytes.on_data(Self::temporary_chunk(chunk, false))?; - self.drop_backpressure_if_unobserved(&readable, &bytes); - } else { - readable.value.ensure_still_alive(); - response.detach_readable_stream(&global_this); - bytes.on_data(Self::temporary_chunk(chunk, true))?; - } + let Some(response) = self.current_response() else { + return Ok(()); + }; + bun_output::scoped_log!(FetchTasklet, "onBodyReceived Current Response"); + let size_hint = self.get_size_hint(); + response.set_size_hint(size_hint); + if let Some(readable) = response.get_body_readable_stream(&global_this) { + bun_output::scoped_log!( + FetchTasklet, + "onBodyReceived CurrentResponse BodyReadableStream" + ); + if let Some(bytes) = readable.ptr.bytes() { + let chunk = self.scheduled_response_buffer.list.as_slice(); - return Ok(()); + if self.result.has_more { + bytes.on_data(Self::temporary_chunk(chunk, false))?; + self.drop_backpressure_if_unobserved(&readable, &bytes); + } else { + readable.value.ensure_still_alive(); + response.detach_readable_stream(&global_this); + bytes.on_data(Self::temporary_chunk(chunk, true))?; } + + return Ok(()); } + } - // we will reach here when not streaming, this is also the only case we dont wanna to reset the buffer - buffer_reset.set(false); - if !self.result.has_more { - let scheduled_response_buffer = - core::mem::take(&mut self.scheduled_response_buffer.list); - // `body` (&mut response.body.value) and `get_fetch_headers()` - // (&response.init.headers) are disjoint fields, but borrowck can't see - // through the accessor methods. Hold `body` as a raw ptr. - let body: *mut BodyValue = response.get_body_value(); - // done resolve body - let old = core::mem::replace( - // SAFETY: just obtained from live `response`; uniquely accessed here. - unsafe { &mut *body }, - BodyValue::InternalBlob(InternalBlob { - bytes: scheduled_response_buffer, - was_string: false, - }), - ); - bun_output::scoped_log!( - FetchTasklet, - "onBodyReceived body_value length={}", - // SAFETY: see above. - match unsafe { &*body } { - BodyValue::InternalBlob(b) => b.bytes.len(), - _ => 0, - } - ); + // we will reach here when not streaming, this is also the only case we dont wanna to reset the buffer + buffer_reset.set(false); + if self.result.has_more { + return Ok(()); + } - self.scheduled_response_buffer = MutableString::default(); - - if matches!(old, BodyValue::Locked(_)) { - bun_output::scoped_log!(FetchTasklet, "onBodyReceived old.resolve"); - let mut old = old; - // BodyValue::resolve takes `Option>` (opaque C++ handle - // mutated via FFI); the inherent `get_fetch_headers` returns `Option<&_>`, so - // erase the borrow into a raw NonNull. Disjoint from `body` (response.init vs - // response.body) and outlives this block. - let headers = response.get_fetch_headers().map(core::ptr::NonNull::from); - // Body.rs aliases its `JsTerminated` to `JsResult` for - // now; narrow back to the real `JsTerminated` here. - // SAFETY: `body` points into `response.body`, disjoint from `headers` - // (response.init); both live for this block. - BodyValue::resolve(&mut old, unsafe { &mut *body }, &self.global_this, headers) - .map_err(|_| bun_jsc::JsTerminated::JSTerminated)?; - } + // Both `&mut self` writes happen before the `&Response` is re-derived; + // a `&self`-tied borrow cannot straddle them. No JS runs in between, so + // the response is still there. + let scheduled_response_buffer = core::mem::take(&mut self.scheduled_response_buffer.list); + self.scheduled_response_buffer = MutableString::default(); + let Some(response) = self.current_response() else { + return Ok(()); + }; + + // `body` (&mut response.body.value) and `get_fetch_headers()` + // (&response.init.headers) are disjoint fields; both accessors take + // `&self`, so the two shared borrows coexist. + let body: &mut BodyValue = response.get_body_value(); + // done resolve body + let old = core::mem::replace( + body, + BodyValue::InternalBlob(InternalBlob { + bytes: scheduled_response_buffer, + was_string: false, + }), + ); + bun_output::scoped_log!( + FetchTasklet, + "onBodyReceived body_value length={}", + match &*body { + BodyValue::InternalBlob(b) => b.bytes.len(), + _ => 0, } + ); + + if matches!(old, BodyValue::Locked(_)) { + bun_output::scoped_log!(FetchTasklet, "onBodyReceived old.resolve"); + let mut old = old; + // Disjoint from `body` (response.init vs response.body). + let headers = response.headers(); + // Body.rs aliases its `JsTerminated` to `JsResult` for + // now; narrow back to the real `JsTerminated` here. + BodyValue::resolve(&mut old, body, &self.global_this, headers) + .map_err(|_| bun_jsc::JsTerminated::JSTerminated)?; } Ok(()) } @@ -1744,8 +1742,7 @@ impl FetchTasklet { let redirected = self.result.redirected; Response::init( crate::webcore::response::Init { - // SAFETY: create_from_pico_headers returns a fresh refcount=1 FetchHeaders*. - headers: Some(unsafe { HeadersRef::adopt(headers) }), + headers: Some(headers), status_code, status_text: status_text.into(), ..Default::default() @@ -1934,9 +1931,10 @@ impl FetchTasklet { fetch_tasklet.signals.cert_errors = None; } - let fetch_tasklet_ptr = bun_core::heap::into_raw(fetch_tasklet); - // SAFETY: just allocated; exclusive access until returned - let fetch_tasklet = unsafe { &mut *fetch_tasklet_ptr }; + // Ownership hands off to the intrusive `ref_count`; the trailing + // `deref()` reclaims the allocation once the count hits zero. + let fetch_tasklet: &mut FetchTasklet = bun_core::heap::release(fetch_tasklet); + let fetch_tasklet_ptr: *mut FetchTasklet = &raw mut *fetch_tasklet; // This task gets queued on the HTTP thread. // `AsyncHTTP::init` takes several `&'static [u8]` borrows diff --git a/src/runtime/webcore/prompt.rs b/src/runtime/webcore/prompt.rs index 480245da05c9..9b54e3980fac 100644 --- a/src/runtime/webcore/prompt.rs +++ b/src/runtime/webcore/prompt.rs @@ -3,6 +3,7 @@ use crate::webcore::jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; use bun_collections::VecExt as _; use bun_core::Output; +use bun_jsc::JsCell; use bun_jsc::ZigStringJsc as _; use bun_jsc::zig_string::ZigString; @@ -307,12 +308,12 @@ pub mod prompt { // 7. Pause while waiting for the user's response. // `bun.Output.buffered_stdin.reader()` — process-global 4 KiB buffered stdin. - // SAFETY: process-global static; prompt() runs single-threaded on the JS - // main thread, so the exclusive borrow is sound for this scope. - let reader: &mut bun_core::output::BufferedStdin = - unsafe { &mut *Output::buffered_stdin_reader() }; + // SAFETY: `JsCell` is `repr(transparent)` over its payload, so the cast is exact; + // the static is main-thread-only and every mutation below is `with_mut`-scoped. + let reader: &JsCell = + unsafe { &*Output::buffered_stdin_reader().cast() }; let mut second_byte: Option = None; - let Ok(first_byte) = reader.read_byte() else { + let Ok(first_byte) = reader.with_mut(|r| r.read_byte()) else { // 8. Let result be null if the user aborts, or otherwise the string // that the user responded with. return Ok(JSValue::NULL); @@ -323,7 +324,7 @@ pub mod prompt { // that the user responded with. return Ok(default); } else if first_byte == b'\r' { - let Ok(second) = reader.read_byte() else { + let Ok(second) = reader.with_mut(|r| r.read_byte()) else { return Ok(JSValue::NULL); }; second_byte = Some(second); @@ -343,12 +344,9 @@ pub mod prompt { // buffer of size 2048. If that is too small, then increase the buffer // size to 4096. If that is too small, then just dynamically allocate // the rest. - if let Err(e) = read_until_delimiter_array_list_append_assume_capacity( - &mut *reader, - &mut input, - b'\n', - 2048, - ) { + if let Err(e) = reader.with_mut(|r| { + read_until_delimiter_array_list_append_assume_capacity(r, &mut input, b'\n', 2048) + }) { if !matches!(e, ReadError::StreamTooLong) { // 8. Let result be null if the user aborts, or otherwise the string // that the user responded with. @@ -357,19 +355,17 @@ pub mod prompt { input.ensure_total_capacity(4096); - if let Err(e2) = read_until_delimiter_array_list_append_assume_capacity( - &mut *reader, - &mut input, - b'\n', - 4096, - ) { + if let Err(e2) = reader.with_mut(|r| { + read_until_delimiter_array_list_append_assume_capacity(r, &mut input, b'\n', 4096) + }) { if !matches!(e2, ReadError::StreamTooLong) { // 8. Let result be null if the user aborts, or otherwise the string // that the user responded with. return Ok(JSValue::NULL); } - if read_until_delimiter_array_list_infinity(&mut *reader, &mut input, b'\n') + if reader + .with_mut(|r| read_until_delimiter_array_list_infinity(r, &mut input, b'\n')) .is_err() { // 8. Let result be null if the user aborts, or otherwise the string diff --git a/src/runtime/webcore/s3/client.rs b/src/runtime/webcore/s3/client.rs index 26bfaa497f63..1fce1bd8cab5 100644 --- a/src/runtime/webcore/s3/client.rs +++ b/src/runtime/webcore/s3/client.rs @@ -602,17 +602,11 @@ impl S3UploadStreamWrapper { } /// Exclusive borrow of the `MultiPartUpload` this wrapper holds a counted - /// ref on (released in `Drop`). Detached lifetime so the borrow does not - /// conflict with disjoint `&mut self` field access at call sites — `task` - /// is a separate heap allocation, not inside `*self`. - /// - /// SAFETY (encapsulated): `task` is set once at construction from - /// `MultiPartUpload::create` and intrusive-ref'd for this wrapper's entire - /// lifetime; single-threaded JS — no overlapping `&mut` from elsewhere. + /// ref on (released in `Drop`). `task` is set once at construction and stays + /// live for this wrapper's entire lifetime. #[inline] - #[allow(clippy::mut_from_ref)] - fn task_mut<'r>(&self) -> &'r mut MultiPartUpload { - // SAFETY: see doc comment — counted ref keeps pointee live; sole writer. + fn task_mut(&mut self) -> &mut MultiPartUpload { + // SAFETY: the counted ref keeps the pointee live; `&mut self` bounds the borrow. unsafe { &mut *self.task } } @@ -1241,14 +1235,12 @@ pub fn readable_stream( // Ownership of the heap-allocated NewSource transfers to the JS wrapper (m_ctx) via // `to_readable_stream()`/`to_js()`; the wrapper's finalize() reclaims it. - let reader: *mut crate::webcore::byte_stream::Source = - crate::webcore::byte_stream::Source::new(crate::webcore::readable_stream::NewSource { + let reader_mut = + crate::webcore::byte_stream::Source::new_mut(crate::webcore::readable_stream::NewSource { context: ByteStream::default(), global_this: Some(bun_ptr::BackRef::new(global_this)), ..Default::default() }); - // SAFETY: freshly heap-allocated via TrivialNew; exclusive access until handed to JS below. - let reader_mut = unsafe { &mut *reader }; reader_mut.context.setup(); let readable_value = reader_mut.to_readable_stream(global_this)?; diff --git a/src/runtime/webcore/s3/simple_request.rs b/src/runtime/webcore/s3/simple_request.rs index 986715dbe109..0e4feb917b53 100644 --- a/src/runtime/webcore/s3/simple_request.rs +++ b/src/runtime/webcore/s3/simple_request.rs @@ -326,19 +326,11 @@ impl S3HttpSimpleTask { } /// this is the task callback from the last task result and is always in the main thread - /// - /// # Safety - /// `this` must be a live heap pointer produced by `S3HttpSimpleTask::new` whose ownership - /// is being transferred to this call (it is reclaimed and dropped here exactly once). // - // ConcurrentTask dispatch entrypoint (see `runtime::dispatch`): `this` is the raw task - // pointer the queue hands back, non-null by the `ConcurrentTask::from` contract. - #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub fn on_response(this: *mut Self) -> JsTerminatedResult<()> { - // SAFETY: `this` was produced by `S3HttpSimpleTask::new` (heap::alloc) and ownership is - // reclaimed here exactly once via the ConcurrentTask `.manual_deinit` contract; - // `this` is dropped at scope exit. - let mut this = unsafe { bun_core::heap::take(this) }; + // ConcurrentTask dispatch entrypoint (see `runtime::dispatch`): ownership of the task + // transfers here and the box is dropped at scope exit. + pub fn on_response(self: Box) -> JsTerminatedResult<()> { + let mut this = self; if !this.result.is_success() { this.error_with_body(ErrorType::Failure)?; diff --git a/src/runtime/webcore/streams.rs b/src/runtime/webcore/streams.rs index 0d86d23081bf..f0c98cf81f62 100644 --- a/src/runtime/webcore/streams.rs +++ b/src/runtime/webcore/streams.rs @@ -1937,28 +1937,19 @@ impl HTTPServerWritable { false } - /// # Safety - /// `this` must be a valid, uniquely-owned heap pointer to `Self` produced - /// by `bun_core::heap::into_raw`; the caller transfers ownership. - // Forwards `this` to `bun_core::heap::take` without dereferencing it here; - // not_unsafe_ptr_arg_deref is a false positive on opaque-token forwarding. - #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub fn destroy(this: *mut Self) { + // `boxed_local`: the `Box` is the ownership unit being reclaimed here. + #[allow(clippy::boxed_local)] + pub fn destroy(mut self: Box) { bun_core::scoped_log!(HTTPServerWritableLog, "destroy()"); - // SAFETY: this was heap-allocated; destroy takes sole ownership. Reclaim - // the Box first so we never hold a `&mut *this` alongside the Box's - // unique pointer. - let mut this = unsafe { bun_core::heap::take(this) }; // Callers may tear this sink down without routing through // flushPromise() (e.g. handleResolveStream / handleRejectStream). // Drop the GC root so the promise can be collected. - if let Some(prom) = this.pending_flush.take() { + if let Some(prom) = self.pending_flush.take() { // S008: `JSPromise` is an `opaque_ffi!` ZST — safe `*const → &` deref. JSPromise::opaque_ref(prom).to_js().unprotect(); } - this.buffer.clear_and_free(); - this.unregister_auto_flusher(); - drop(this); + self.buffer.clear_and_free(); + self.unregister_auto_flusher(); } /// This can be called _many_ times for the same instance @@ -2289,18 +2280,11 @@ impl NetworkSink { )) } - /// # Safety - /// `this` must be a valid, uniquely-owned heap pointer to `Self` produced - /// by `bun_core::heap::into_raw`; the caller transfers ownership. - // Forwards `this` to `bun_core::heap::take` without dereferencing it here; - // not_unsafe_ptr_arg_deref is a false positive on opaque-token forwarding. - #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub fn finalize_and_destroy(this: *mut Self) { - // SAFETY: this was heap-allocated; reclaim sole ownership before - // touching fields so no `&mut *this` is live alongside the Box. - let mut this = unsafe { bun_core::heap::take(this) }; - this.finalize(); - drop(this); + /// Takes ownership of the sink and frees it after detaching the upload. + // `boxed_local`: the `Box` is the ownership unit being reclaimed here. + #[allow(clippy::boxed_local)] + pub fn finalize_and_destroy(mut self: Box) { + self.finalize(); } pub fn abort(&mut self) { @@ -2404,7 +2388,7 @@ impl NetworkSink { } pub fn to_js(&mut self, global_this: &JSGlobalObject) -> JSValue { - NetworkSinkJSSink::create_object(global_this, self, 0) + NetworkSinkJSSink::create_object(global_this, std::ptr::from_mut(self), 0) } pub fn memory_cost(&self) -> usize { diff --git a/src/runtime/webcore/wasm_streaming.rs b/src/runtime/webcore/wasm_streaming.rs index 839d1aa19356..4fc41208623a 100644 --- a/src/runtime/webcore/wasm_streaming.rs +++ b/src/runtime/webcore/wasm_streaming.rs @@ -32,11 +32,11 @@ pub(crate) fn get_body_stream_or_bytes_for_wasm_streaming( response_value: JSValue, streaming_compiler: *mut c_void, ) -> JsResult { - let response: &mut Response = match response::from_js(response_value) { + let response: &Response = match response::from_js(response_value) { // SAFETY: `from_js` returns a pointer to the GC-owned `Response` cell; // the cell stays live for the duration of this host call (rooted on the // C++ caller's stack). - Some(r) => unsafe { &mut *r }, + Some(r) => unsafe { &*r }, None => { return Err(this.throw_invalid_argument_type_value2( b"source", diff --git a/src/sourcemap_jsc/CodeCoverage.rs b/src/sourcemap_jsc/CodeCoverage.rs index 4211602d374e..69f2a8ad5ea3 100644 --- a/src/sourcemap_jsc/CodeCoverage.rs +++ b/src/sourcemap_jsc/CodeCoverage.rs @@ -1,4 +1,3 @@ -use core::cell::UnsafeCell; use core::ffi::{c_int, c_void}; use core::ptr::NonNull; @@ -6,7 +5,7 @@ use bun_ast::Loc; use bun_collections::VecExt; use bun_collections::bit_set::DynamicBitSet; use bun_core::{self, ZigStringSlice, strings}; -use bun_jsc::{JSGlobalObject, JSValue, VM, bun_string_jsc}; +use bun_jsc::{JSGlobalObject, JSValue, JsCell, VM, bun_string_jsc}; use bun_sourcemap::{ LineOffsetTable, LineOffsetTableColumns as _, Ordinal, ParsedSourceMap, internal_source_map, line_offset_table, @@ -360,7 +359,7 @@ unsafe extern "C" { source_id: i32, ctx: *mut c_void, ignore_sourcemap: bool, - cb: extern "C" fn(*mut Generator, *const BasicBlockRange, usize, usize, bool), + cb: extern "C" fn(*mut c_void, *const BasicBlockRange, usize, usize, bool), ) -> bool; } @@ -370,16 +369,15 @@ struct Generator<'a> { } impl<'a> Generator<'a> { + /// Trampoline for the `(cb, ctx)` pair handed to `CodeCoverage__withBlocksAndFunctions`. + /// The `*mut c_void` is dereferenced exactly here; `run` is a safe method. extern "C" fn do_( - this: *mut Generator, + ctx: *mut c_void, blocks_ptr: *const BasicBlockRange, blocks_len: usize, function_start_offset: usize, ignore_sourcemap: bool, ) { - // SAFETY: `this` was passed as &mut Generator to CodeCoverage__withBlocksAndFunctions - // and is valid for the duration of this synchronous callback. - let this = unsafe { &mut *this }; // The C++ side (CodeCoverage.cpp) invokes this callback with `(nullptr, 0, 0)` when // basicBlocks is empty. `core::slice::from_raw_parts` requires a non-null, aligned // pointer even for zero-length slices, so we must bail before constructing the slice. @@ -389,8 +387,20 @@ impl<'a> Generator<'a> { // SAFETY: blocks_len != 0, so blocks_ptr[0..blocks_len] is a valid contiguous C array // provided by JSC for the duration of this synchronous callback. let all = unsafe { core::slice::from_raw_parts(blocks_ptr, blocks_len) }; + // SAFETY: `ctx` is the `&raw mut generator` passed alongside this callback; it is the + // only pointer to that `Generator`, which outlives this synchronous callback. + let this = unsafe { &mut *ctx.cast::>() }; + this.run(all, function_start_offset, ignore_sourcemap); + } + + fn run( + &mut self, + all: &[BasicBlockRange], + function_start_offset: usize, + ignore_sourcemap: bool, + ) { let blocks: &[BasicBlockRange] = &all[0..function_start_offset]; - let mut function_blocks: &[BasicBlockRange] = &all[function_start_offset..blocks_len]; + let mut function_blocks: &[BasicBlockRange] = &all[function_start_offset..]; if function_blocks.len() > 1 { function_blocks = &function_blocks[1..]; } @@ -403,8 +413,8 @@ impl<'a> Generator<'a> { // `from_utf8_never_free` already detaches the lifetime by design, and // `generate_report_from_blocks` only borrows `&self`, so no &/&mut overlap. let source_url = - ZigStringSlice::from_utf8_never_free(this.byte_range_mapping.source_url.slice()); - *this.result = this + ZigStringSlice::from_utf8_never_free(self.byte_range_mapping.source_url.slice()); + *self.result = self .byte_range_mapping .generate_report_from_blocks(source_url, blocks, function_blocks, ignore_sourcemap) .ok(); @@ -438,33 +448,22 @@ thread_local! { // `*mut ByteRangeMapping` pointing into it). The Box is **owned** by the // thread-local — it is dropped on thread exit, never leaked (PORTING.md // §Forbidden: no Box::leak). - static MAP: UnsafeCell>> = - const { UnsafeCell::new(None) }; + static MAP: JsCell>> = const { JsCell::new(None) }; } -/// Returns a raw pointer to this thread's map, lazily creating it. -/// The pointer is valid until thread exit (the Box is pinned in the thread-local -/// slot and never moved or dropped earlier). -fn thread_map() -> *mut ByteRangeMappingHashMap { +/// Runs `f` with this thread's map, lazily creating it. `f` must not re-enter +/// this thread-local (it holds the only `&mut` to the map for its duration). +fn thread_map(f: impl FnOnce(&mut ByteRangeMappingHashMap) -> R) -> R { MAP.with(|cell| { - // SAFETY: thread-local; no other reference to this UnsafeCell can exist - // concurrently on this thread while we hold this exclusive borrow. - let slot = unsafe { &mut *cell.get() }; - if slot.is_none() { - *slot = Some(Box::new(ByteRangeMappingHashMap::default())); - } - // SAFETY: just ensured Some above; Box deref gives stable address. - &raw mut **slot.as_mut().unwrap() + cell.with_mut(|slot| { + f(slot.get_or_insert_with(|| Box::new(ByteRangeMappingHashMap::default()))) + }) }) } -/// Returns a raw pointer to this thread's map if it has been created, else null. +/// Returns a pointer to this thread's map if it has been created, else `None`. fn thread_map_opt() -> Option> { - MAP.with(|cell| { - // SAFETY: thread-local exclusive access. - let slot = unsafe { &mut *cell.get() }; - slot.as_mut().map(|b| NonNull::from(&mut **b)) - }) + MAP.with(|cell| cell.with_mut(|slot| slot.as_mut().map(|b| NonNull::from(&mut **b)))) } impl ByteRangeMapping { @@ -847,17 +846,15 @@ pub(crate) extern "C" fn ByteRangeMapping__generate( source_contents_str: bun_core::String, source_id: i32, ) { - // SAFETY: thread_map() returns a pointer into this thread's owned Box; - // valid for the lifetime of the thread, and we are the only mutable accessor on - // this thread for the duration of this call. - let map = unsafe { &mut *thread_map() }; - let slice = str_.to_utf8(); let hash = bun_wyhash::hash(slice.slice()); let source_contents = source_contents_str.to_utf8(); + // Build the value before borrowing the map: nothing may run inside `thread_map`. let new_value = ByteRangeMapping::compute(source_contents.slice(), source_id, slice); - map.insert(hash, new_value); + thread_map(|map| { + map.insert(hash, new_value); + }); // `source_contents` drops here (matches `defer source_contents.deinit()`). // Note: `slice` ownership transferred into the new ByteRangeMapping.source_url. } @@ -872,13 +869,14 @@ pub(crate) extern "C" fn ByteRangeMapping__find( path: bun_core::String, ) -> Option> { let slice = path.to_utf8(); - - let map_ptr = thread_map_opt()?; - // SAFETY: map_ptr points into this thread's owned Box; valid until thread exit. - let map = unsafe { &mut *map_ptr.as_ptr() }; let hash = bun_wyhash::hash(slice.slice()); - let entry = map.get_mut(&hash)?; - Some(NonNull::from(entry)) + + MAP.with(|cell| { + cell.with_mut(|slot| { + let entry = slot.as_mut()?.get_mut(&hash)?; + Some(NonNull::from(entry)) + }) + }) } #[unsafe(no_mangle)] diff --git a/src/spawn/process.rs b/src/spawn/process.rs index 55f602c37671..a590c5ea5fb3 100644 --- a/src/spawn/process.rs +++ b/src/spawn/process.rs @@ -438,8 +438,10 @@ impl Process { self.poller = Poller::Fd( core::ptr::NonNull::new(poll).expect("FilePoll::init returns a live hive slot"), ); - // SAFETY: poll is live; exclusive on this thread (event loop). - let fd = unsafe { &mut *poll }; + let fd = self + .poller + .fd_poll_mut() + .expect("poller was just set to Poller::Fd"); fd.enable_keeping_process_alive(ctx); // SAFETY: `platform_event_loop` returns the live uws loop. @@ -2389,8 +2391,11 @@ mod spawn_process_body { pending_alloc: Option>, pub pipe: Box, pub err: bun_sys::E, - pub context: *mut SyncWindowsProcess, - pub on_done_callback: fn(*mut SyncWindowsProcess, OutFd, Vec>, bun_sys::E), + /// Non-owning back-ref: the parent outlives every reader (it spins on + /// `waiting_count` before reclaiming itself). + pub context: bun_ptr::ParentRef, + pub on_done_callback: + fn(bun_ptr::ParentRef, OutFd, Vec>, bun_sys::E), pub tag: OutFd, } @@ -2441,9 +2446,9 @@ mod spawn_process_body { suggested_size: usize, buffer: *mut uv::uv_buf_t, ) { - // SAFETY: `req.data` was set to `*mut Self` in `start()`. - let this: &mut SyncWindowsPipeReader = - unsafe { &mut *((*req).data as *mut SyncWindowsPipeReader) }; + // SAFETY: `req.data` was set to `*mut Self` in `start()`; libuv fires + // this from the loop with no other borrow of the reader live. + let this = unsafe { bun_ptr::callback_ctx::((*req).data) }; let buf = Self::on_alloc(this, suggested_size); // SAFETY: `buffer` is a libuv-owned out-parameter. Do NOT route // through `uv_buf_t::init(&[u8])` — that reborrows the `&mut [u8]` @@ -2463,9 +2468,9 @@ mod spawn_process_body { nreads: uv::ReturnCodeI64, buffer: *const uv::uv_buf_t, ) { - // SAFETY: `req.data` was set to `*mut Self` in `start()`. - let this: &mut SyncWindowsPipeReader = - unsafe { &mut *((*req).data as *mut SyncWindowsPipeReader) }; + // SAFETY: `req.data` was set to `*mut Self` in `start()`; libuv fires + // this from the loop with no other borrow of the reader live. + let this = unsafe { bun_ptr::callback_ctx::((*req).data) }; let nreads = nreads.int(); if nreads == 0 { return; @@ -2497,24 +2502,23 @@ mod spawn_process_body { !this.is_null(), "Expected SyncWindowsPipeReader to have data" ); - // SAFETY: this is valid until we destroy it below - let this_ref = unsafe { &mut *this }; - let context = this_ref.context; + // SAFETY: heap-allocated in `start()`; uv fires this close callback + // exactly once, so reclaiming the Box here is the sole owner. + let mut this = unsafe { bun_core::heap::take(this) }; + let context = this.context; // Move ownership of the chunk allocations out *before* dropping // `this`, otherwise the callback would observe freed buffers. // The chunk allocations survive to be freed later by // `flatten_owned_chunks`. - let chunks: Vec> = core::mem::take(&mut this_ref.chunks); - let err = if this_ref.err == bun_sys::E::CANCELED { + let chunks: Vec> = core::mem::take(&mut this.chunks); + let err = if this.err == bun_sys::E::CANCELED { bun_sys::E::SUCCESS } else { - this_ref.err + this.err }; - let tag = this_ref.tag; - let on_done_callback = this_ref.on_done_callback; - // bun.default_allocator.destroy(this) - // SAFETY: this was heap-allocated in start(); reclaim and drop - drop(unsafe { bun_core::heap::take(this) }); + let tag = this.tag; + let on_done_callback = this.on_done_callback; + drop(this); on_done_callback(context, tag, chunks, err); } @@ -2612,13 +2616,15 @@ mod spawn_process_body { } pub fn on_reader_done( - this: *mut SyncWindowsProcess, + this: bun_ptr::ParentRef, tag: OutFd, chunks: Vec>, err: bun_sys::E, ) { - // SAFETY: this is valid (back-ref from SyncWindowsPipeReader) - let this = unsafe { &mut *this }; + // SAFETY: built by `from_raw_mut` (write provenance). The reader that + // held this back-ref was freed by `on_close` before we got here, and + // the uv loop is single-threaded, so no other borrow of the parent lives. + let this = unsafe { this.assume_mut() }; match tag { OutFd::Stderr => this.stderr = chunks, OutFd::Stdout => this.stdout = chunks, @@ -2736,7 +2742,9 @@ mod spawn_process_body { let taken = core::mem::replace(stdio, WindowsStdioResult::Unavailable); if let WindowsStdioResult::Buffer(pipe) = taken { let reader = SyncWindowsPipeReader::new(SyncWindowsPipeReader { - context: this_ptr, + // SAFETY: `this_ptr` is the live `heap::alloc` root (mutable + // provenance) and outlives the reader. + context: unsafe { bun_ptr::ParentRef::from_raw_mut(this_ptr) }, tag, pipe, chunks: Vec::new(), diff --git a/src/spawn/static_pipe_writer.rs b/src/spawn/static_pipe_writer.rs index 78489596650e..6eacbd2baead 100644 --- a/src/spawn/static_pipe_writer.rs +++ b/src/spawn/static_pipe_writer.rs @@ -119,7 +119,8 @@ impl StaticPipeWriter

{ result: StdioResult, source: Source, ) -> IntrusiveRc { - let this = bun_core::heap::into_raw(Box::new(Self { + #[cfg_attr(not(windows), allow(unused_mut))] + let mut this = Box::new(Self { ref_count: RefCount::init(), writer: IOWriter::

::default(), stdio_result: result, @@ -128,9 +129,7 @@ impl StaticPipeWriter

{ event_loop, started: false, buffer: RawSlice::EMPTY, - })); - // SAFETY: `this` was just allocated above and is non-null. - let this_ref = unsafe { &mut *this }; + }); #[cfg(windows)] { // On Windows `StdioResult` is the `WindowsStdioResult` union and @@ -140,18 +139,21 @@ impl StaticPipeWriter

{ // `Source::Pipe`, so we move it out (replacing with `Unavailable`) // and `heap::alloc` it (set_pipe re-wraps via `heap::take`). use crate::process::WindowsStdioResult; - match core::mem::replace(&mut this_ref.stdio_result, WindowsStdioResult::Unavailable) { + match core::mem::replace(&mut this.stdio_result, WindowsStdioResult::Unavailable) { WindowsStdioResult::Buffer(pipe) => { // SAFETY: `pipe` is a Box-allocated `uv::Pipe`; `set_pipe` // takes ownership via `heap::take`. - unsafe { this_ref.writer.set_pipe(bun_core::heap::into_raw(pipe)) }; + unsafe { this.writer.set_pipe(bun_core::heap::into_raw(pipe)) }; } WindowsStdioResult::BufferFd(_) | WindowsStdioResult::Unavailable => { unreachable!("StaticPipeWriter stdin requires WindowsStdioResult::Buffer"); } } } - this_ref.writer.set_parent(this); + let this = bun_core::heap::into_raw(this); + // SAFETY: `this` is the allocation we just gave up above; nothing else + // aliases it yet, and the backref it stores is `this` itself. + unsafe { (*this).writer.set_parent(this) }; // SAFETY: ownership of the initial ref is transferred to the returned IntrusiveRc. unsafe { IntrusiveRc::from_raw(this) } } diff --git a/src/sql_jsc/jsc.rs b/src/sql_jsc/jsc.rs index 86f1698eefd1..8dd4788e34e7 100644 --- a/src/sql_jsc/jsc.rs +++ b/src/sql_jsc/jsc.rs @@ -274,8 +274,13 @@ pub(crate) trait VirtualMachineSqlExt { /// shadowing the inherent VirtualMachine::rare_data() (which returns the /// bun_jsc RareData holding the per-protocol SocketGroups). fn sql_state(&mut self) -> &mut RareData; - /// vm.timer — the Timer::All heap, owned by RuntimeState. - fn timer(&mut self) -> &mut TimerHeap; + /// Shared view of [`Self::sql_state`] for read-only users such as + /// `StrongOptional::get`; mints no exclusive reference to the VM singleton. + fn sql_state_ref(&self) -> &RareData; + /// vm.timer — the Timer::All heap, owned by RuntimeState. Shared borrow: + /// the heap is a disjoint RuntimeState allocation, not VM storage, and + /// `TimerHeap`'s own methods take `&self`. + fn timer(&self) -> &TimerHeap; /// RareData.ssl_ctx_cache — owned by RuntimeState. fn ssl_ctx_cache(&mut self) -> &mut SslCtxCache; /// bun_io::EventLoopCtx for the JS-thread VM, for KeepAlive::{ref_,unref}. @@ -300,11 +305,19 @@ impl VirtualMachineSqlExt for VirtualMachine { unsafe { &mut *(hooks().sql_rare)(self) } } #[inline] - fn timer(&mut self) -> &mut TimerHeap { - // SAFETY: hook returns `&mut runtime_state().timer`; non-null after - // `init_runtime_state`. `TimerHeap` is an opaque newtype over the - // `*mut c_void` so callers stay typed. - unsafe { &mut *(hooks().timer_heap)(self).cast::() } + fn sql_state_ref(&self) -> &RareData { + // SAFETY: the hook ignores its argument and returns + // `&runtime_state().sql_rare`; non-null after `init_runtime_state`. + // Provenance is the thread-local `*mut`, not a cast of `&self`. + unsafe { &*(hooks().sql_rare)(VirtualMachine::get_mut_ptr()) } + } + #[inline] + fn timer(&self) -> &TimerHeap { + debug_assert!(core::ptr::eq(self, VirtualMachine::get_mut_ptr())); + // SAFETY: hook returns `runtime_state().timer`; non-null after + // `init_runtime_state`. Provenance is the thread-local `*mut` from + // `init()`, mirroring `VirtualMachine::as_mut`. + unsafe { &*(hooks().timer_heap)(VirtualMachine::get_mut_ptr()).cast::() } } #[inline] fn ssl_ctx_cache(&mut self) -> &mut SslCtxCache { @@ -373,13 +386,15 @@ pub use bun_event_loop::EventLoopTimer::{ // [`SqlRuntimeHooks`] vtable. bun_opaque::opaque_ffi! { pub struct TimerHeap; } impl TimerHeap { - pub fn insert(&mut self, t: &mut EventLoopTimer) { - // SAFETY: `self` is `&mut runtime_state().timer`; `t` is a live + // `&self`: the handle is an opaque `UnsafeCell` ZST at the address of + // `runtime_state().timer`; the mutation happens across the FFI boundary. + pub fn insert(&self, t: &mut EventLoopTimer) { + // SAFETY: `self` is `&runtime_state().timer`; `t` is a live // intrusive heap node owned by the caller. unsafe { (hooks().timer_insert)(self._p.get().cast::(), t) } } - pub fn remove(&mut self, t: &mut EventLoopTimer) { - // SAFETY: `self` is `&mut runtime_state().timer`; `t` was previously + pub fn remove(&self, t: &mut EventLoopTimer) { + // SAFETY: `self` is `&runtime_state().timer`; `t` was previously // inserted by the caller. unsafe { (hooks().timer_remove)(self._p.get().cast::(), t) } } diff --git a/src/sql_jsc/mysql/JSMySQLConnection.rs b/src/sql_jsc/mysql/JSMySQLConnection.rs index fd3510456792..dc44873cc48e 100644 --- a/src/sql_jsc/mysql/JSMySQLConnection.rs +++ b/src/sql_jsc/mysql/JSMySQLConnection.rs @@ -55,8 +55,7 @@ pub struct JSMySQLConnection { // LIFETIMES.tsv: JSC_BORROW — assigned from createInstance param; never freed global_object: GlobalRef, // LIFETIMES.tsv: STATIC — globalObject.bunVM() singleton. `BackRef` so the - // hot `vm()` deref is safe; `vm_mut()` routes through the canonical - // `VirtualMachine::as_mut()` accessor. + // hot `vm()` deref is safe; no `&mut VirtualMachine` is ever formed here. vm: BackRef, poll_ref: JsCell, @@ -124,23 +123,14 @@ impl JSMySQLConnection { fn vm(&self) -> &VirtualMachine { self.vm.get() } - /// Short-lived `&mut VirtualMachine` for the few `vm.timer()` callers - /// (jsc shim's `timer()` is `&mut self`). The VM is a JS-thread singleton; - /// we never hold two `&mut` to it at once in this module. - fn vm_mut(&self) -> &'static mut VirtualMachine { - VirtualMachine::get_mut() - } - - /// `&mut EventLoop` for `entered()`/`run_callback`. One audited unsafe - /// here replaces the per-site `unsafe { self.vm().event_loop_mut() }` — - /// the loop is a disjoint heap allocation owned by the JS-thread VM - /// singleton; single-thread affinity ⇒ no two `&mut EventLoop` coexist. + /// `&mut EventLoop` for `entered()`/`run_callback`. The loop is a disjoint + /// heap allocation owned by the JS-thread VM singleton; single-thread + /// affinity ⇒ no two `&mut EventLoop` coexist. #[inline] fn event_loop(&self) -> &'static mut crate::jsc::EventLoop { - // `vm_mut()` yields the process-lifetime `'static mut VM` (see above); - // the owned event loop lives for the VM's lifetime. Single-JS-thread - // invariant ⇒ callers never overlap `&mut`. - self.vm_mut().event_loop_mut() + // `VirtualMachine::get()` is the `&'static` JS-thread singleton; the + // owned event loop lives for the VM's lifetime. + VirtualMachine::get().event_loop_mut() } #[inline] @@ -222,11 +212,11 @@ impl JSMySQLConnection { fn stop_timers(&self) { bun_core::scoped_log!(MySQLConnection, "stopTimers"); if self.timer.get().state == EventLoopTimerState::ACTIVE { - self.timer.with_mut(|t| self.vm_mut().timer().remove(t)); + self.timer.with_mut(|t| self.vm().timer().remove(t)); } if self.max_lifetime_timer.get().state == EventLoopTimerState::ACTIVE { self.max_lifetime_timer - .with_mut(|t| self.vm_mut().timer().remove(t)); + .with_mut(|t| self.vm().timer().remove(t)); } } @@ -247,7 +237,7 @@ impl JSMySQLConnection { let interval = self.get_timeout_interval(); bun_core::scoped_log!(MySQLConnection, "resetConnectionTimeout {}", interval); if self.timer.get().state == EventLoopTimerState::ACTIVE { - self.timer.with_mut(|t| self.vm_mut().timer().remove(t)); + self.timer.with_mut(|t| self.vm().timer().remove(t)); } if self.connection.get().status == my_sql_connection::Status::Failed || self.connection.get().is_processing_data() @@ -258,7 +248,7 @@ impl JSMySQLConnection { self.timer.with_mut(|t| { t.next = timespec::ms_from_now(TimespecMockMode::AllowMockedTime, interval.into()); - self.vm_mut().timer().insert(t); + self.vm().timer().insert(t); }); } @@ -338,7 +328,7 @@ impl JSMySQLConnection { TimespecMockMode::AllowMockedTime, self.max_lifetime_interval_ms.into(), ); - self.vm_mut().timer().insert(t); + self.vm().timer().insert(t); }); } diff --git a/src/sql_jsc/mysql/JSMySQLQuery.rs b/src/sql_jsc/mysql/JSMySQLQuery.rs index bfdb3025ea48..5718f185f1b5 100644 --- a/src/sql_jsc/mysql/JSMySQLQuery.rs +++ b/src/sql_jsc/mysql/JSMySQLQuery.rs @@ -37,7 +37,6 @@ bun_core::define_scoped_log!(debug, MySQLQuery); // shim still emits `this: &mut JSMySQLQuery` — `&mut T` auto-derefs to `&T` // so the impls below compile against either. #[derive(bun_ptr::CellRefCounted)] -#[ref_count(destroy = Self::deinit)] pub struct JSMySQLQuery { this_value: JsCell, // unfortunately we cannot use #ref_count here @@ -49,8 +48,14 @@ pub struct JSMySQLQuery { } // Intrusive refcount (bun.ptr.RefCount): `ref_()`/`deref()` provided by -// `#[derive(CellRefCounted)]`; `destroy` routes to `Self::deinit` via the -// struct-level `#[ref_count(destroy = …)]` attribute. +// `#[derive(CellRefCounted)]`; the default `destroy` is `heap::take(this)`, +// which runs the `Drop` below before freeing. + +impl Drop for JSMySQLQuery { + fn drop(&mut self) { + self.query.with_mut(|q| q.cleanup()); + } +} impl JSMySQLQuery { /// RAII `ref()`/`deref()` bracket around `self`. One audited @@ -77,15 +82,6 @@ impl JSMySQLQuery { .throw_invalid_arguments(format_args!("MySQLQuery cannot be constructed directly"))) } - fn deinit(this: *mut Self) { - // SAFETY: routed only through `CellRefCounted::destroy` (refcount==0); - // `this` is the sole live owner of its `heap::alloc` allocation. - unsafe { - (*this).query.with_mut(|q| q.cleanup()); - drop(bun_core::heap::take(this)); - } - } - pub fn finalize(self: Box) { debug!("MySQLQuery finalize"); bun_ptr::finalize_js_box(self, |this| this.this_value.with_mut(|v| v.finalize())); @@ -269,8 +265,8 @@ impl JSMySQLQuery { js_tag.ensure_still_alive(); let Some(function) = self - .vm_mut() - .sql_state() + .vm() + .sql_state_ref() .mysql_context .on_query_resolve_fn .get() @@ -364,8 +360,8 @@ impl JSMySQLQuery { debug_assert!(!js_error.is_empty(), "js_error is zero"); js_error.ensure_still_alive(); let Some(function) = self - .vm_mut() - .sql_state() + .vm() + .sql_state_ref() .mysql_context .on_query_reject_fn .get() @@ -571,10 +567,6 @@ impl JSMySQLQuery { fn vm(&self) -> &VirtualMachine { self.vm.get() } - #[inline] - fn vm_mut(&self) -> &'static mut VirtualMachine { - VirtualMachine::get_mut() - } /// `&mut EventLoop` for `run_callback`. Routes through the inherent safe /// `VirtualMachine::event_loop_mut` accessor — the loop is a disjoint heap /// allocation owned by the JS-thread VM singleton stored in `self.vm`; diff --git a/src/sql_jsc/mysql/MySQLConnection.rs b/src/sql_jsc/mysql/MySQLConnection.rs index d4c9e9686653..4e7e8d89c0dc 100644 --- a/src/sql_jsc/mysql/MySQLConnection.rs +++ b/src/sql_jsc/mysql/MySQLConnection.rs @@ -54,7 +54,7 @@ pub struct MySQLConnection { write_buffer: OffsetByteList, read_buffer: OffsetByteList, - last_message_start: u32, + last_message_start: core::cell::Cell, sequence_id: u8, // TODO: move it to JSMySQLConnection @@ -97,7 +97,7 @@ impl Default for MySQLConnection { status: ConnectionState::Disconnected, write_buffer: OffsetByteList::default(), read_buffer: OffsetByteList::default(), - last_message_start: 0, + last_message_start: core::cell::Cell::new(0), sequence_id: 0, queue: MySQLRequestQueue::init(), statements: PreparedStatementsMap::default(), @@ -504,7 +504,7 @@ impl MySQLConnection { ); self.read_buffer.head = 0; - self.last_message_start = 0; + self.last_message_start.set(0); self.read_buffer.byte_list.clear(); self.read_buffer .write(&data[offset.get()..]) @@ -521,7 +521,7 @@ impl MySQLConnection { } { - self.read_buffer.head = self.last_message_start; + self.read_buffer.head = self.last_message_start.get(); self.read_buffer .write(data) @@ -544,7 +544,7 @@ impl MySQLConnection { bun_core::scoped_log!( MySQLConnection, "Received short read: last_message_start: {}, head: {}, len: {}", - self.last_message_start, + self.last_message_start.get(), self.read_buffer.head, self.read_buffer.byte_list.len() ); @@ -555,7 +555,7 @@ impl MySQLConnection { } } - self.last_message_start = 0; + self.last_message_start.set(0); self.read_buffer.head = 0; } self.flags.remove(ConnectionFlags::IS_PROCESSING_DATA); @@ -1402,6 +1402,7 @@ impl MySQLConnection { }; if !statement .execution_flags + .get() .contains(mysql_statement::ExecutionFlags::HEADER_RECEIVED) { if packet_type == PacketType::OK { @@ -1460,12 +1461,11 @@ impl MySQLConnection { statement.cached_structure = Default::default(); statement.fields_flags = Default::default(); } - statement - .execution_flags - .insert(mysql_statement::ExecutionFlags::NEEDS_DUPLICATE_CHECK); - statement - .execution_flags - .insert(mysql_statement::ExecutionFlags::HEADER_RECEIVED); + statement.execution_flags.set( + statement.execution_flags.get() + | mysql_statement::ExecutionFlags::NEEDS_DUPLICATE_CHECK + | mysql_statement::ExecutionFlags::HEADER_RECEIVED, + ); return Ok(()); } else if (statement.columns_received as usize) < statement.columns.len() { let changed = statement.columns[statement.columns_received as usize] @@ -1473,9 +1473,10 @@ impl MySQLConnection { if changed { statement.cached_structure = Default::default(); statement.fields_flags = Default::default(); - statement - .execution_flags - .insert(mysql_statement::ExecutionFlags::NEEDS_DUPLICATE_CHECK); + statement.execution_flags.set( + statement.execution_flags.get() + | mysql_statement::ExecutionFlags::NEEDS_DUPLICATE_CHECK, + ); } statement.columns_received += 1; } else { @@ -1498,14 +1499,16 @@ impl MySQLConnection { // the final EOF (after all rows) differently. if !statement .execution_flags + .get() .contains(mysql_statement::ExecutionFlags::COLUMNS_EOF_RECEIVED) { // Intermediate EOF between column definitions and row data - skip it let mut eof = EOFPacket::default(); eof.decode_internal(reader)?; - statement - .execution_flags - .insert(mysql_statement::ExecutionFlags::COLUMNS_EOF_RECEIVED); + statement.execution_flags.set( + statement.execution_flags.get() + | mysql_statement::ExecutionFlags::COLUMNS_EOF_RECEIVED, + ); return Ok(()); } // Final EOF after all row data - terminates the result set @@ -1577,43 +1580,48 @@ pub struct Writer { } impl Writer { + /// # Safety + /// + /// `self.connection` must point at a live `MySQLConnection`, and no other + /// reference to its `write_buffer` field — including one reached through + /// the connection's own `&mut self`, or through another `Writer` copy — + /// may be live while the returned reference is used. `Writer` is `Copy`, + /// so nothing checks this. #[inline] #[allow(clippy::mut_from_ref)] - fn write_buffer(&self) -> &mut OffsetByteList { - // SAFETY: `self.connection` is never null — `Writer` is only ever - // constructed by `MySQLConnection::writer(&mut self)` from a live - // `&mut MySQLConnection`, and the `NewWriter` is consumed - // before that connection is dropped (it is never stored). - // - // Raw-pointer field projection (`addr_of_mut!`) avoids materializing - // an intermediate `&mut MySQLConnection`, which could alias the - // caller's own `&mut self` (see the aliasing note on `Reader` below). - // Callers never touch `write_buffer` through `&mut self` while a - // `Writer` is live, so no two `&mut OffsetByteList` coexist. + unsafe fn write_buffer(&self) -> &mut OffsetByteList { + // SAFETY: field projection through a valid `*mut MySQLConnection` per + // the contract above. `addr_of_mut!` avoids materializing an + // intermediate `&mut MySQLConnection` that would alias the caller's. unsafe { &mut *core::ptr::addr_of_mut!((*self.connection).write_buffer) } } } impl WriterContext for Writer { fn write(self, data: &[u8]) -> Result<(), AnyMySQLError> { - self.write_buffer() - .write(data) - .map_err(|_| AnyMySQLError::OutOfMemory)?; + // SAFETY: connection outlives this call; the reference dies here. + let buffer = unsafe { self.write_buffer() }; + buffer.write(data).map_err(|_| AnyMySQLError::OutOfMemory)?; Ok(()) } fn pwrite(self, data: &[u8], index: usize) -> Result<(), AnyMySQLError> { - let byte_list = &mut self.write_buffer().byte_list; + // SAFETY: connection outlives this call; the reference dies here. + let buffer = unsafe { self.write_buffer() }; + let byte_list = &mut buffer.byte_list; byte_list.slice_mut()[index..][..data.len()].copy_from_slice(data); Ok(()) } fn offset(self) -> usize { - self.write_buffer().len() as usize + // SAFETY: connection outlives this call; the reference dies here. + let buffer = unsafe { self.write_buffer() }; + buffer.len() as usize } fn truncate(self, offset: usize) { - let buffer = self.write_buffer(); + // SAFETY: connection outlives this call; the reference dies here. + let buffer = unsafe { self.write_buffer() }; let head = buffer.head as usize; buffer.byte_list.truncate(head + offset); } @@ -1646,23 +1654,20 @@ impl Reader { } #[inline] - #[allow(clippy::mut_from_ref)] - fn last_message_start(&self) -> &mut u32 { - // SAFETY: same justification as `read_buffer()` — disjoint field - // projection from a non-null connection pointer that outlives the - // `Reader`; `process_packets` does not access `last_message_start` - // through `&mut self` while the reader is live. - unsafe { &mut *core::ptr::addr_of_mut!((*self.connection).last_message_start) } + fn last_message_start(&self) -> &core::cell::Cell { + // SAFETY: `self.connection` points at a live `MySQLConnection` for the + // duration of the read call; the `Reader` is never stored. + unsafe { &*core::ptr::addr_of!((*self.connection).last_message_start) } } } impl ReaderContext for Reader { fn mark_message_start(self) { - *self.last_message_start() = self.read_buffer().head; + self.last_message_start().set(self.read_buffer().head); } fn set_offset_from_start(self, offset: usize) { - self.read_buffer().head = *self.last_message_start() + (offset as u32); + self.read_buffer().head = self.last_message_start().get() + (offset as u32); } fn peek(&self) -> &[u8] { diff --git a/src/sql_jsc/mysql/MySQLQuery.rs b/src/sql_jsc/mysql/MySQLQuery.rs index bdf673992985..870e035e74f6 100644 --- a/src/sql_jsc/mysql/MySQLQuery.rs +++ b/src/sql_jsc/mysql/MySQLQuery.rs @@ -211,11 +211,12 @@ impl MySQLQuery { columns_value: JSValue, roots: &mut MarkedArgumentBuffer, ) -> Result<(), AnyMySQLError> { - // SAFETY: `statement` was copied from `self.statement` by `run_prepared_query`; - // the intrusive ref held there keeps the allocation alive across this call. The - // caller passes the raw pointer before reborrowing `self`, so this is the only - // live mutable access path to the statement for the duration of this function. - let statement = unsafe { &mut *statement }; + // Non-null, and the intrusive ref in `self.statement` keeps it alive across this + // call. Access is shared-only: `execution_flags` is a `Cell`, so `self.bind` + // below (which can run user JS) never spans a `&mut` to the statement. + let statement = bun_ptr::ParentRef::from( + core::ptr::NonNull::new(statement).expect("bind_and_execute_impl: statement non-null"), + ); // Bind before touching the writer so a bind failure (user-triggerable via JS // getters / param-count mismatch) doesn't leave a partial packet header in @@ -252,6 +253,7 @@ impl MySQLQuery { param_types: &statement.signature.fields, new_params_bind_flag: statement .execution_flags + .get() .contains(ExecutionFlags::NEED_TO_SEND_PARAMS), params: ExecuteParams { len: params.len(), @@ -265,9 +267,12 @@ impl MySQLQuery { let mut packet = writer.start(0)?; execute.write(writer)?; packet.end()?; - statement - .execution_flags - .remove(ExecutionFlags::NEED_TO_SEND_PARAMS); + statement.execution_flags.set( + statement + .execution_flags + .get() + .difference(ExecutionFlags::NEED_TO_SEND_PARAMS), + ); self.status = Status::Running; Ok(()) } diff --git a/src/sql_jsc/mysql/MySQLStatement.rs b/src/sql_jsc/mysql/MySQLStatement.rs index 95914708d6c3..4815ee5eae4b 100644 --- a/src/sql_jsc/mysql/MySQLStatement.rs +++ b/src/sql_jsc/mysql/MySQLStatement.rs @@ -33,7 +33,7 @@ pub struct MySQLStatement { pub signature: Signature, pub status: Status, pub error_response: ErrorPacket, - pub execution_flags: ExecutionFlags, + pub execution_flags: Cell, pub fields_flags: DataCellFlags, pub result_count: u64, } @@ -52,7 +52,7 @@ impl MySQLStatement { signature, status, error_response: ErrorPacket::default(), - execution_flags: ExecutionFlags::default(), + execution_flags: Cell::new(ExecutionFlags::default()), fields_flags: DataCellFlags::default(), result_count: 0, } @@ -103,18 +103,22 @@ impl MySQLStatement { pub(crate) fn reset(&mut self) { self.result_count = 0; self.columns_received = 0; - self.execution_flags = ExecutionFlags::default(); + self.execution_flags.set(ExecutionFlags::default()); } pub(crate) fn check_for_duplicate_fields(&mut self) { if !self .execution_flags + .get() .contains(ExecutionFlags::NEEDS_DUPLICATE_CHECK) { return; } - self.execution_flags - .remove(ExecutionFlags::NEEDS_DUPLICATE_CHECK); + self.execution_flags.set( + self.execution_flags + .get() + .difference(ExecutionFlags::NEEDS_DUPLICATE_CHECK), + ); self.fields_flags = dedupe_columns(self.columns.iter_mut().rev().map(|c| &mut c.name_or_index)); diff --git a/src/sql_jsc/postgres/PostgresSQLConnection.rs b/src/sql_jsc/postgres/PostgresSQLConnection.rs index 68e12e871659..2f6151107088 100644 --- a/src/sql_jsc/postgres/PostgresSQLConnection.rs +++ b/src/sql_jsc/postgres/PostgresSQLConnection.rs @@ -220,19 +220,6 @@ impl PostgresSQLConnection { VirtualMachine::get_mut() } - /// `&mut EventLoop` for `enter`/`exit`/`run_callback`. One audited unsafe - /// here replaces the per-site `unsafe { self.vm().event_loop_mut() }` — - /// the loop is a disjoint heap allocation owned by the JS-thread VM - /// singleton (see [`vm_mut`]); single-thread affinity ⇒ no two - /// `&mut EventLoop` ever coexist. - #[inline] - fn event_loop(&self) -> &'static mut crate::jsc::EventLoop { - // `vm_mut()` yields the process-lifetime `'static mut VM` (see above); - // the event loop it owns lives for the VM's lifetime. Single-JS-thread - // invariant ⇒ callers never hold two `&mut EventLoop` at once. - self.vm_mut().event_loop_mut() - } - /// `KeepAlive::{ref_,unref}` take an `EventLoopCtx` (manual vtable, lives in /// `bun_io`). The sql_jsc-side `VirtualMachine` is a thin façade with no /// direct conversion; route through the global hook (`get_vm_ctx(.Js)`) which @@ -276,19 +263,20 @@ impl PostgresSQLConnection { } /// Project `&mut SASL` from `authentication_state` if it is currently the - /// `Sasl` variant. One audited [`JsCell::get_mut`] here replaces the three - /// per-site unchecked `authentication_state.get_mut()` derefs in the SASL - /// handshake arms of [`on`](Self::on). + /// `Sasl` variant. The returned borrow is tied to `&self`. + /// + /// # Safety /// - /// SAFETY (encapsulated): single-JS-thread; callers hold the returned - /// `&mut SASL` only for the synchronous packet-handler body and drop it - /// before any call that touches `authentication_state` again - /// (`self.writer()` / `self.flush_data()` / `self.fail()` do not). + /// Caller must be on the owning JS thread and must guarantee that no other + /// reference to `authentication_state` — a second `sasl_state_mut`, or a + /// `JsCell` `get`/`set`/`with_mut` — is live for the returned borrow's + /// lifetime. In particular the borrow must not be held across any call that + /// can re-enter JS (e.g. `self.fail()`), which may read the cell and + /// invalidate it. `self.writer()` and `self.flush_data()` do not. #[inline] - #[allow(clippy::mut_from_ref)] // body projects through `JsCell` (UnsafeCell-backed); see SAFETY note - fn sasl_state_mut(&self) -> Option<&mut crate::postgres::sasl::SASL> { - // SAFETY: see doc comment — single-JS-thread, no re-entrant access to - // `authentication_state` for the borrow's lifetime. + #[allow(clippy::mut_from_ref)] // projection through `JsCell` (UnsafeCell-backed) + unsafe fn sasl_state_mut(&self) -> Option<&mut crate::postgres::sasl::SASL> { + // SAFETY: forwarded to the caller — see the fn-level contract. match unsafe { self.authentication_state.get_mut() } { AuthenticationState::Sasl(s) => Some(s), _ => None, @@ -716,8 +704,7 @@ impl PostgresSQLConnection { // we defer the refAndClose so the on_close will be called first before we reject the pending requests let on_close_opt = self.consume_on_close_callback(self.global()); if let Some(on_close) = on_close_opt { - let event_loop = self.event_loop(); - event_loop.enter(); + let _exit = self.vm().enter_event_loop_scope(); let mut js_error = value.to_error().unwrap_or(value); if js_error.is_empty() { js_error = postgres_error_to_js( @@ -731,7 +718,6 @@ impl PostgresSQLConnection { if let Err(e) = on_close.call(self.global(), JSValue::UNDEFINED, &[js_error, queries]) { self.global().report_active_exception_as_unhandled(e); } - event_loop.exit(); } self.ref_and_close(Some(value)); // SAFETY: `self` is a live Box-allocated connection; this releases one ref. @@ -807,12 +793,10 @@ impl PostgresSQLConnection { self.clean_up_requests(None); self.update_has_pending_activity(); } else { - let event_loop = self.event_loop(); - event_loop.enter(); + let _exit = self.vm().enter_event_loop_scope(); self.poll_ref.with_mut(|r| r.unref(self.vm_ctx())); fail(self); - event_loop.exit(); } } @@ -957,8 +941,7 @@ impl PostgresSQLConnection { return self.close(); } - let event_loop = self.event_loop(); - event_loop.enter(); + let _exit = self.vm().enter_event_loop_scope(); self.flush_data(); @@ -970,7 +953,6 @@ impl PostgresSQLConnection { self.advance(); self.flush_data(); } - event_loop.exit(); } pub fn on_data(&self, data: &[u8]) { @@ -979,8 +961,7 @@ impl PostgresSQLConnection { self.disable_connection_timeout(); - let event_loop = self.event_loop(); - event_loop.enter(); + let exit_scope = self.vm().enter_event_loop_scope(); SocketMonitor::read(data); // reset the head to the last message so remaining reflects the right amount of bytes self.read_buffer @@ -1052,7 +1033,7 @@ impl PostgresSQLConnection { } } - event_loop.exit(); + drop(exit_scope); // === defer block === if self.status.get() == Status::Connected && !self.has_query_running() @@ -2643,9 +2624,10 @@ impl PostgresSQLConnection { } let mut mechanism_buf = [0u8; 128]; - // `sasl` borrow ends before `self.writer()`/`self.flush_data()` - // below (neither touches `authentication_state`). - let Some(sasl) = self.sasl_state_mut() else { + // SAFETY: no other borrow of `authentication_state` is live; + // `sasl` ends before `self.writer()`/`self.flush_data()` below + // (neither touches the cell nor runs JS). + let Some(sasl) = (unsafe { self.sasl_state_mut() }) else { unreachable!() }; let mechanism = { @@ -2667,9 +2649,10 @@ impl PostgresSQLConnection { } protocol::Authentication::SASLContinue(cont) => { let password: &[u8] = self.password(); - // `sasl` borrow ends before `self.writer()`/`self.flush_data()` - // below (neither touches `authentication_state`). - let Some(sasl) = self.sasl_state_mut() else { + // SAFETY: no other borrow of `authentication_state` is live; + // `sasl` ends before `self.writer()`/`self.flush_data()` below + // (neither touches the cell nor runs JS). + let Some(sasl) = (unsafe { self.sasl_state_mut() }) else { debug!("Unexpected SASLContinue for authentication state"); return Err(AnyPostgresError::UnexpectedMessage); }; @@ -2788,9 +2771,10 @@ impl PostgresSQLConnection { self.flush_data(); } protocol::Authentication::SASLFinal { data: final_data } => { - // `sasl` borrow ends before `self.fail()` / + // SAFETY: no other borrow of `authentication_state` is live, and + // `sasl` is dead before `self.fail()` (runs JS) and before // `self.authentication_state.with_mut()` below. - let Some(sasl) = self.sasl_state_mut() else { + let Some(sasl) = (unsafe { self.sasl_state_mut() }) else { debug!("SASLFinal - Unexpected SASLContinue for authentication state"); return Err(AnyPostgresError::UnexpectedMessage); }; diff --git a/src/sql_jsc/postgres/PostgresSQLQuery.rs b/src/sql_jsc/postgres/PostgresSQLQuery.rs index 11ed2722ba68..06d88c02a3a9 100644 --- a/src/sql_jsc/postgres/PostgresSQLQuery.rs +++ b/src/sql_jsc/postgres/PostgresSQLQuery.rs @@ -7,7 +7,7 @@ use crate::jsc::{ use crate::shared::query_ctor_args::QueryCtorArgs; use bun_core::String as BunString; use bun_jsc::JsCell; -use bun_ptr::AsCtxPtr; +use bun_ptr::{AsCtxPtr, RefPtr}; use super::PostgresSQLConnection; use super::PostgresSQLStatement; @@ -38,7 +38,9 @@ pub use js::to_js; // previous `from_mut(self)` raw-pointer dances papered over. #[derive(bun_ptr::CellRefCounted)] pub struct PostgresSQLQuery { - pub statement: Cell>, + // Owned strong ref. `RefPtr` has no `Drop`: the ref is discharged by + // `release_statement`, which `Drop for PostgresSQLQuery` calls. + pub statement: JsCell>>, pub query: BunString, pub cursor_name: BunString, @@ -69,7 +71,7 @@ impl Drop for PostgresSQLQuery { impl Default for PostgresSQLQuery { fn default() -> Self { Self { - statement: Cell::new(None), + statement: JsCell::new(None), query: BunString::empty(), cursor_name: BunString::empty(), this_value: JsCell::new(JsRef::empty()), @@ -133,36 +135,34 @@ impl PostgresSQLQuery { unsafe { bun_ptr::ScopedRef::new(self.as_ctx_ptr()) } } - /// Dereference the intrusive `statement` pointer as `&mut`. Mirrors - /// [`MySQLQuery::get_statement`]: one unchecked deref here replaces N inline - /// raw-pointer derefs at every protocol dispatch site in + /// The `&mut` view of the owned `statement`: one audited deref replaces the + /// inline raw-pointer derefs at every dispatch site in /// `PostgresSQLConnection::on`. /// - /// SAFETY (encapsulated): when `Some`, the pointer is a live `heap::alloc` - /// payload kept alive by the intrusive ref this query holds (`ref_()` taken - /// at `statement.set(Some(_))`). All mutation is single-JS-thread so the - /// `&mut` is exclusive for the borrow's lifetime; callers must not hold two - /// results live simultaneously (the request FIFO never does). + /// SAFETY (encapsulated): the owned `RefPtr` keeps the pointee alive, and + /// `RefPtr::as_ptr` is its sanctioned mutation path (a `&mut` accessor would + /// alias every other handle). Single-JS-thread ⇒ exclusive for the borrow's + /// lifetime; callers must not hold two results live simultaneously (the + /// request FIFO never does), nor hold one across a call that re-enters JS. #[inline] - #[allow(clippy::mut_from_ref)] // intrusive raw pointer; see SAFETY in doc comment + #[allow(clippy::mut_from_ref)] // shared-ownership handle; see SAFETY in doc comment pub fn statement_mut(&self) -> Option<&mut PostgresSQLStatement> { - // SAFETY: see doc comment — intrusive ref held by `self` keeps the - // pointee alive; single-JS-thread exclusivity. - self.statement.get().map(|p| unsafe { &mut *p }) + // SAFETY: see doc comment. + self.statement + .get() + .as_ref() + .map(|stmt| unsafe { &mut *stmt.as_ptr() }) } - /// Release the intrusive ref this query holds on its `statement`, clearing - /// the field. One audited deref here replaces the per-site - /// `this.statement.set(None)` + `PostgresSQLStatement::deref(stmt)` pair on - /// `Drop` and `do_run`'s error paths (6 callers). + /// Release the ref this query owns on its `statement`, clearing the field. + /// `RefPtr` has no `Drop`, so this is the sole discharge point: `Drop` and + /// `do_run`'s error paths (6 callers). #[inline] pub fn release_statement(&self) { - if let Some(stmt) = self.statement.take() { - // SAFETY: when `Some`, `stmt` is a live `heap::alloc` payload kept - // alive by the intrusive ref this query took when it was stored - // into `self.statement` (`ref_()` / `init_exact_refs`). This - // releases exactly that ref; may free if no other refs remain. - unsafe { PostgresSQLStatement::deref(stmt) }; + // The `&mut Option<_>` is confined to the closure; the `deref()` (which + // may run `PostgresSQLStatement::drop`) runs after that borrow ends. + if let Some(stmt) = self.statement.with_mut(|s| s.take()) { + stmt.deref(); } } @@ -518,11 +518,11 @@ impl PostgresSQLQuery { // hand ownership to `this.statement` (count = 1). // NOTE: PostgresSQLStatement implements Drop, so functional-record-update // (`..Default::default()`) is forbidden (E0509). Build + mutate instead. - let stmt: *mut PostgresSQLStatement = { + let stmt = { let mut s = PostgresSQLStatement::default(); s.signature = Signature::empty(); s.status = StatementStatus::Parsing; - bun_core::heap::into_raw(Box::new(s)) + RefPtr::new(s) }; // Query is simple and it's the only owner of the statement this.statement.set(Some(stmt)); @@ -623,22 +623,25 @@ impl PostgresSQLQuery { .get(&signature.name[..]) .copied(); if let Some(stmt_ptr) = existing_stmt { - this.statement.set(Some(stmt_ptr)); - // Route the `&mut` through the audited `statement_mut()` - // accessor (just set above ⇒ `Some`); `stmt_ptr` is kept - // only for the explicit `deref(stmt_ptr)` cleanup below. - let stmt = this.statement_mut().expect("statement set above"); - stmt.ref_(); + // SAFETY: `stmt_ptr` is a live entry of `connection.statements`; + // `init_ref` takes the ref this query owns until `release_statement`. + this.statement + .set(Some(unsafe { RefPtr::init_ref(stmt_ptr) })); + // Shared borrow only: `bind_and_execute` below reaches JS, so no + // `&mut` to the statement may be live across it. + let stmt = this + .statement + .get() + .as_deref() + .expect("statement set above"); drop(signature); match stmt.status { StatementStatus::Failed => { - this.statement.set(None); // `error_response` is `Some` when status == Failed. let error_response = stmt.error_response.as_ref().unwrap().to_js(global_object)?; - // SAFETY: drop the ref we took above. - unsafe { PostgresSQLStatement::deref(stmt_ptr) }; + this.release_statement(); // SAFETY: undoes the speculative `this.ref_()` above; count was ≥2, never frees here. unsafe { Self::deref(this_ptr) }; return Err(global_object.throw_value(error_response)); @@ -795,21 +798,21 @@ impl PostgresSQLQuery { let stmt = { let mut s = PostgresSQLStatement::default(); s.signature = signature; - s.init_exact_refs(2); s.status = if did_write { StatementStatus::Parsing } else { StatementStatus::Pending }; - bun_core::heap::into_raw(Box::new(s)) + RefPtr::new(s) }; - this.statement.set(Some(stmt)); // SAFETY: `entry_value` points into `connection.statements` and the map has // not been mutated since `get_or_put`. `get_or_put` runs only after the // existing-entry probe missed, so the slot it hands back was // default-initialised to null and a plain store is fine. - unsafe { *entry_value = stmt }; + // `dupe_ref().into_raw()` hands the map its own owned ref. + unsafe { *entry_value = stmt.dupe_ref().into_raw() }; + this.statement.set(Some(stmt)); } else { let stmt = { let mut s = PostgresSQLStatement::default(); @@ -819,7 +822,7 @@ impl PostgresSQLQuery { } else { StatementStatus::Pending }; - bun_core::heap::into_raw(Box::new(s)) + RefPtr::new(s) }; this.statement.set(Some(stmt)); } diff --git a/src/sys/lib.rs b/src/sys/lib.rs index 8be6c71b8db3..9e9b51a1baea 100644 --- a/src/sys/lib.rs +++ b/src/sys/lib.rs @@ -9458,52 +9458,71 @@ impl SysQuietWriterAdapter { #[inline] fn buffered(&self) -> &[u8] { // SAFETY: `buf` is a `cap`-byte allocation owned by this adapter (set - // at construction); `pos <= cap` is upheld by `adapter_write_all` + // at construction); `pos <= cap` is upheld by `write_all` // (drains before writing past `cap`). Bytes `[0, pos)` were written by // `copy_nonoverlapping` and are initialized. Borrow tied to `&self`. unsafe { core::slice::from_raw_parts(self.buf, self.pos) } } } -unsafe fn adapter_write_all( +/// Safe body of an `io::Writer` adapter. `Self` must be `repr(C)` with +/// `io::Writer` as its first field; the trampolines below do the one downcast. +trait WriterImpl: Sized { + fn write_all(&mut self, bytes: &[u8]); + fn flush(&mut self); +} + +unsafe fn tramp_write_all( w: *mut bun_core::io::Writer, bytes: &[u8], ) -> core::result::Result<(), bun_core::Error> { - // SAFETY: `w` points at the first field of a SysQuietWriterAdapter (repr(C)). - let this = unsafe { &mut *w.cast::() }; - if this.cap == 0 { - let _ = fd_write_all_quiet(this.fd, bytes); - return Ok(()); - } - if this.pos + bytes.len() > this.cap { - // Drain buffered bytes first. - if this.pos > 0 { - let _ = fd_write_all_quiet(this.fd, this.buffered()); - this.pos = 0; + // SAFETY: `w` is the repr(C) head of the `T` this instantiation was + // registered on; the `&mut T` is the only live reference for the call. + unsafe { &mut *w.cast::() }.write_all(bytes); + Ok(()) +} + +unsafe fn tramp_flush( + w: *mut bun_core::io::Writer, +) -> core::result::Result<(), bun_core::Error> { + // SAFETY: as `tramp_write_all`. + unsafe { &mut *w.cast::() }.flush(); + Ok(()) +} + +impl WriterImpl for SysQuietWriterAdapter { + fn write_all(&mut self, bytes: &[u8]) { + if self.cap == 0 { + let _ = fd_write_all_quiet(self.fd, bytes); + return; } - // Large writes bypass the buffer so the next small write still coalesces. - if bytes.len() >= this.cap { - let _ = fd_write_all_quiet(this.fd, bytes); - return Ok(()); + if self.pos + bytes.len() > self.cap { + // Drain buffered bytes first. + if self.pos > 0 { + let _ = fd_write_all_quiet(self.fd, self.buffered()); + self.pos = 0; + } + // Large writes bypass the buffer so the next small write still coalesces. + if bytes.len() >= self.cap { + let _ = fd_write_all_quiet(self.fd, bytes); + return; + } } + // SAFETY: `self.buf` has capacity `self.cap`; the branch above ensures + // `self.pos + bytes.len() <= self.cap`, so `[buf+pos, buf+pos+len)` is + // in-bounds and cannot overlap `bytes` (caller-owned slice). + unsafe { + core::ptr::copy_nonoverlapping(bytes.as_ptr(), self.buf.add(self.pos), bytes.len()); + } + self.pos += bytes.len(); } - // SAFETY: `this.buf` has capacity `this.cap`; the branch above ensures - // `this.pos + bytes.len() <= this.cap`, so `[buf+pos, buf+pos+len)` is - // in-bounds and cannot overlap `bytes` (caller-owned slice). - unsafe { - core::ptr::copy_nonoverlapping(bytes.as_ptr(), this.buf.add(this.pos), bytes.len()); - } - this.pos += bytes.len(); - Ok(()) -} -unsafe fn adapter_flush(w: *mut bun_core::io::Writer) -> core::result::Result<(), bun_core::Error> { - // SAFETY: `w` points at the first field of a SysQuietWriterAdapter (repr(C)). - let this = unsafe { &mut *w.cast::() }; - if this.pos > 0 { - let _ = fd_write_all_quiet(this.fd, this.buffered()); - this.pos = 0; + + fn flush(&mut self) { + if self.pos > 0 { + let _ = fd_write_all_quiet(self.fd, self.buffered()); + self.pos = 0; + } } - Ok(()) } #[cfg(unix)] @@ -9545,8 +9564,8 @@ bun_core::link_impl_OutputSink! { let fd = qw_fd(&qw); let concrete = SysQuietWriterAdapter { writer: bun_core::io::Writer { - write_all: adapter_write_all, - flush: adapter_flush, + write_all: tramp_write_all::, + flush: tramp_flush::, }, fd, buf, diff --git a/src/threading/ThreadPool.rs b/src/threading/ThreadPool.rs index 8b7763139958..ca905782304e 100644 --- a/src/threading/ThreadPool.rs +++ b/src/threading/ThreadPool.rs @@ -524,19 +524,24 @@ impl ThreadPool { i: usize, } - // `run_fn` is stored in WaitContext and dispatched via the `EachCall` - // trait (ByValue vs ByPtr). + impl> RunnerTask { + /// `run_fn` is stored in WaitContext and dispatched via the `EachCall` + /// trait (ByValue vs ByPtr). + fn run(&self) { + let wctx = self.ctx.get(); + // SAFETY: `values` slice outlives all RunnerTasks (wait_for_all() blocks until + // every task finishes); each task owns a distinct index `i`. + let value: *mut V = unsafe { &raw mut (*wctx.values)[self.i] }; + // SAFETY: `value` is live and exclusively owned by this task per the index. + unsafe { wctx.run_fn.call(&wctx.ctx, value, self.i) }; + } + } + unsafe fn call>(task: *mut Task) { // SAFETY: task points to RunnerTask.task (offset 0, repr(C)). - let runner_task = - unsafe { &mut *bun_core::from_field_ptr!(RunnerTask, task, task) }; - let i = runner_task.i; - let wctx = runner_task.ctx.get(); - // SAFETY: `values` slice outlives all RunnerTasks (wait_for_all() blocks until - // every task finishes); each task owns a distinct index `i`. - let value: *mut V = unsafe { &raw mut (*wctx.values)[i] }; - // SAFETY: `value` is live and exclusively owned by this task per the index. - unsafe { wctx.run_fn.call(&wctx.ctx, value, i) }; + let runner_task: &RunnerTask = + unsafe { &*bun_core::from_field_ptr!(RunnerTask, task, task) }; + runner_task.run(); } let wait_context = WaitContext { diff --git a/src/uws_sys/Cargo.toml b/src/uws_sys/Cargo.toml index 89683b7631a9..0a9a0b5deda7 100644 --- a/src/uws_sys/Cargo.toml +++ b/src/uws_sys/Cargo.toml @@ -11,6 +11,7 @@ workspace = true [dependencies] bun_opaque.workspace = true +bun_ptr.workspace = true thiserror.workspace = true strum.workspace = true bstr.workspace = true diff --git a/src/uws_sys/ListenSocket.rs b/src/uws_sys/ListenSocket.rs index db68a98429ae..8d6f3ef280b0 100644 --- a/src/uws_sys/ListenSocket.rs +++ b/src/uws_sys/ListenSocket.rs @@ -2,6 +2,7 @@ use core::ffi::{c_char, c_int, c_void}; use core::ptr::NonNull; use bun_core::Fd; +use bun_ptr::ParentRef; use crate::{LIBUS_SOCKET_DESCRIPTOR, SocketGroup, SslCtx, us_socket_t}; @@ -27,10 +28,10 @@ impl ListenSocket { } pub fn get_socket(&mut self) -> &mut us_socket_t { - // SAFETY: ListenSocket is layout-compatible with us_socket_t on the C side - // (a listen socket IS a us_socket_t). The returned - // borrow reborrows `&mut self` exclusively — no alias is live while it exists. - unsafe { &mut *std::ptr::from_mut::(self).cast::() } + // S008: ListenSocket is layout-compatible with us_socket_t on the C side + // (a listen socket IS a us_socket_t); both are `opaque_ffi!` ZSTs, so route + // the `*mut → &mut` pun through the const-asserted safe accessor. + us_socket_t::opaque_mut(std::ptr::from_mut::(self).cast::()) } pub fn socket(&mut self) -> crate::socket::NewSocketHandler { @@ -40,10 +41,12 @@ impl ListenSocket { )) } - /// Group accepted sockets are linked into. - pub fn group(&mut self) -> &mut SocketGroup { - // SAFETY: self is a valid listen socket; C returns a non-null group. - unsafe { &mut *us_listen_socket_group(self) } + /// Group accepted sockets are linked into. The group is embedded in the + /// owner and outlives every listen socket linked into it. + pub fn group(&mut self) -> ParentRef { + // SAFETY: C returns a non-null group, with write provenance, that + // outlives this listen socket. + unsafe { ParentRef::from_raw_mut(us_listen_socket_group(self)) } } pub fn ext(&mut self) -> &mut T { diff --git a/src/uws_sys/Loop.rs b/src/uws_sys/Loop.rs index 8a6a20a8320b..be9b36fe6758 100644 --- a/src/uws_sys/Loop.rs +++ b/src/uws_sys/Loop.rs @@ -431,25 +431,12 @@ impl WindowsLoop { unsafe { &*self.uv_loop } } - /// Exclusive borrow of the backing libuv loop. Used only for the - /// `active_handles` bookkeeping field (Bun-private; libuv itself only - /// reads it inside `uv__loop_alive`). `&mut self` provides exclusivity - /// over the wrapper; the `uv_loop_t` is the per-thread singleton so no - /// other Rust `&mut` to it is live on this thread. - #[inline] - fn uv_mut(&mut self) -> &mut uv::Loop { - // SAFETY: see `uv()` for liveness; `&mut self` is the sole Rust - // borrow path to the wrapper, and the only mutation performed via - // this accessor is the `active_handles` counter. - unsafe { &mut *self.uv_loop } - } - pub fn add_active(&mut self, val: u32) { - self.uv_mut().add_active(val); + self.uv().add_active(val); } pub fn sub_active(&mut self, val: u32) { - self.uv_mut().sub_active(val); + self.uv().sub_active(val); } pub fn is_active(&self) -> bool { @@ -509,11 +496,11 @@ impl WindowsLoop { } pub fn inc(&mut self) { - self.uv_mut().inc(); + self.uv().inc(); } pub fn dec(&mut self) { - self.uv_mut().dec(); + self.uv().dec(); } #[inline] diff --git a/src/uws_sys/WebSocket.rs b/src/uws_sys/WebSocket.rs index 6093f9c8b456..0517c363c2ac 100644 --- a/src/uws_sys/WebSocket.rs +++ b/src/uws_sys/WebSocket.rs @@ -18,6 +18,14 @@ pub struct NewWebSocket { _m: PhantomData<(*mut u8, PhantomPinned)>, } +/// Shared cork trampoline: `user_data` is the `(&mut C, fn(&mut C))` pair on +/// the corking stack frame, alive for the synchronous `uws_ws_cork` call. +extern "C" fn cork_thunk(user_data: *mut c_void) { + // SAFETY: uws_ws_cork forwards, unchanged, the pointer to that live pair. + let data = unsafe { bun_core::callback_ctx::<(&mut C, fn(&mut C))>(user_data) }; + (data.1)(&mut *data.0); +} + impl NewWebSocket { /// Reborrow as the un-parameterized handle type. Both `NewWebSocket<_>` and /// `RawWebSocket` are `#[repr(C)]` opaque ZSTs over `UnsafeCell<[u8; 0]>`, @@ -109,23 +117,13 @@ impl NewWebSocket { /// Rust cannot const-generic over a fn value, so we tunnel /// `(ctx, callback)` through the user-data pointer. pub fn cork(&mut self, ctx: &mut C, callback: fn(&mut C)) { - // Safe fn item: nested local thunk, only coerced to the C-ABI - // fn-pointer type passed to C; body wraps its raw-ptr ops explicitly. - extern "C" fn wrap(user_data: *mut c_void) { - // SAFETY: user_data is &mut (ptr, fn) on the caller's stack frame, - // which outlives the synchronous uws_ws_cork call. - let data = unsafe { bun_core::callback_ctx::<(*mut C, fn(&mut C))>(user_data) }; - // SAFETY: `data.0` was set from `&mut C` on the enclosing `cork` - // stack frame, which outlives this synchronous callback. - (data.1)(unsafe { &mut *data.0 }); - } - let mut data: (*mut C, fn(&mut C)) = (std::ptr::from_mut::(ctx), callback); + let mut data: (&mut C, fn(&mut C)) = (ctx, callback); // `data` lives on this stack frame for the duration of the synchronous - // uws_ws_cork call; the shim only forwards the pointer back to `wrap`. + // uws_ws_cork call; the shim only forwards the pointer to `cork_thunk`. c::uws_ws_cork( SSL_FLAG, self.raw(), - Some(wrap::), + Some(cork_thunk::), (&raw mut data).cast::(), ) } @@ -314,22 +312,12 @@ impl AnyWebSocket { // See NewWebSocket::cork — same fn-pointer tunneling. pub fn cork(self, ctx: &mut C, callback: fn(&mut C)) { - // Safe fn item: nested local thunk, only coerced to the C-ABI - // fn-pointer type passed to C; body wraps its raw-ptr ops explicitly. - extern "C" fn wrap(user_data: *mut c_void) { - // SAFETY: user_data points at a stack tuple alive for the duration - // of the synchronous uws_ws_cork call. - let data = unsafe { bun_core::callback_ctx::<(*mut C, fn(&mut C))>(user_data) }; - // SAFETY: `data.0` was set from `&mut C` on the enclosing `cork` - // stack frame, which outlives this synchronous callback. - (data.1)(unsafe { &mut *data.0 }); - } - let mut data: (*mut C, fn(&mut C)) = (std::ptr::from_mut::(ctx), callback); + let mut data: (&mut C, fn(&mut C)) = (ctx, callback); let ud = (&raw mut data).cast::(); let (ssl, ws) = self.split(); // `data` lives on this stack frame for the duration of the synchronous - // uws_ws_cork call; the shim only forwards `ud` back to `wrap`. - c::uws_ws_cork(ssl, ws, Some(wrap::), ud) + // uws_ws_cork call; the shim only forwards `ud` to `cork_thunk`. + c::uws_ws_cork(ssl, ws, Some(cork_thunk::), ud) } pub fn subscribe(self, topic: &[u8]) -> bool { diff --git a/src/uws_sys/quic/Stream.rs b/src/uws_sys/quic/Stream.rs index ef565b63ac60..503eecfafb5c 100644 --- a/src/uws_sys/quic/Stream.rs +++ b/src/uws_sys/quic/Stream.rs @@ -1,6 +1,7 @@ //! `us_quic_stream_t` — one bidirectional HTTP/3 request stream. Valid //! until its `on_stream_close` callback returns. +use core::cell::Cell; use core::ffi::{c_int, c_uint, c_void}; use core::ptr::NonNull; @@ -65,13 +66,11 @@ impl Stream { unsafe { us_quic_stream_header(self, i).as_ref() } } - pub fn ext(&mut self) -> &mut Option> { + pub fn ext(&mut self) -> &Cell>> { // SAFETY: self is a valid us_quic_stream_t; ext slot is pointer-sized & pointer-aligned, - // and Option> has nullable-pointer layout. - // Aliasing: the ext slot is disjoint storage returned by C (not overlapping the - // zero-sized opaque `Stream` handle), and the returned &mut borrows from &mut self - // so no second &mut to the slot can be obtained while this one is live. - unsafe { &mut *us_quic_stream_ext(self).cast::>>() } + // and Option> has nullable-pointer layout. `Cell` is repr(transparent), so no + // &mut into the slot is ever live across a callback that re-enters lsquic. + unsafe { &*us_quic_stream_ext(self).cast::>>>() } } pub fn write(&mut self, data: &[u8]) -> c_int { diff --git a/src/watcher/Watcher.rs b/src/watcher/Watcher.rs index 0c7edc5e83c8..f498a4ac67c8 100644 --- a/src/watcher/Watcher.rs +++ b/src/watcher/Watcher.rs @@ -75,7 +75,26 @@ pub struct AnyResolveWatcher { pub callback: fn(*mut (), dir_path: &[u8], dir_fd: Fd), } +/// Receiver of resolver directory-watch callbacks. +pub trait ResolveWatchTarget { + fn on_watch_directory(&mut self, dir_path: &[u8], dir_fd: Fd); +} + impl AnyResolveWatcher { + /// Erase a typed context pointer; `tramp` recovers `T` exactly once. + pub fn new(context: *mut T) -> Self { + fn tramp(ctx: *mut (), dir_path: &[u8], dir_fd: Fd) { + // SAFETY: `ctx` is the `*mut T` paired with this trampoline in + // `new`, and `watch` only ever feeds that pointer back. + let this = unsafe { &mut *ctx.cast::() }; + this.on_watch_directory(dir_path, dir_fd); + } + Self { + context: context.cast::<()>(), + callback: tramp::, + } + } + #[inline] pub fn watch(self, dir_path: &[u8], dir_fd: Fd) { (self.callback)(self.context, dir_path, dir_fd) @@ -940,17 +959,7 @@ impl Watcher { } pub fn get_resolve_watcher(&mut self) -> AnyResolveWatcher { - fn wrap(ctx: *mut (), dir_path: &[u8], dir_fd: Fd) { - // SAFETY: ctx was stored from *mut Watcher in get_resolve_watcher() - // and `AnyResolveWatcher::watch` only ever feeds back the paired - // `context`; the resolver holds it for the Watcher's lifetime. - let this = unsafe { &mut *ctx.cast::() }; - Watcher::on_maybe_watch_directory(this, dir_path, dir_fd); - } - AnyResolveWatcher { - context: std::ptr::from_mut::(self).cast::<()>(), - callback: wrap, - } + AnyResolveWatcher::new(std::ptr::from_mut::(self)) } pub fn on_maybe_watch_directory(watch: &mut Self, file_path: &[u8], dir_fd: Fd) { @@ -965,6 +974,12 @@ impl Watcher { } } +impl ResolveWatchTarget for Watcher { + fn on_watch_directory(&mut self, dir_path: &[u8], dir_fd: Fd) { + Watcher::on_maybe_watch_directory(self, dir_path, dir_fd); + } +} + // ─── WatchEvent ─────────────────────────────────────────────────────────── #[derive(Clone, Copy, Default)] diff --git a/src/watcher/lib.rs b/src/watcher/lib.rs index 3a98dbec286f..3767bb750ead 100644 --- a/src/watcher/lib.rs +++ b/src/watcher/lib.rs @@ -32,8 +32,9 @@ pub mod watcher_impl; pub use WatchItemKind as Kind; pub use watcher_impl::{ AnyResolveWatcher, ChangedFilePath, Event, HashType, Item, ItemList, MAX_COUNT, - MAX_EVICTION_COUNT, Op, PackageJSON, REQUIRES_FILE_DESCRIPTORS, WATCH_OPEN_FLAGS, WatchEvent, - WatchItem, WatchItemColumns, WatchItemIndex, WatchItemKind, WatchList, Watcher, WatcherContext, + MAX_EVICTION_COUNT, Op, PackageJSON, REQUIRES_FILE_DESCRIPTORS, ResolveWatchTarget, + WATCH_OPEN_FLAGS, WatchEvent, WatchItem, WatchItemColumns, WatchItemIndex, WatchItemKind, + WatchList, Watcher, WatcherContext, }; // ─── upward-crate placeholders (CYCLEBREAK) ─────────────────────────────── diff --git a/test/js/bun/http/bun-server.test.ts b/test/js/bun/http/bun-server.test.ts index 4be490e0b093..d72eb5d254d0 100644 --- a/test/js/bun/http/bun-server.test.ts +++ b/test/js/bun/http/bun-server.test.ts @@ -336,6 +336,25 @@ describe.concurrent("Server", () => { } }); + test("server.fetch(url, { headers }) copies the Headers instead of aliasing them", async () => { + using server = Bun.serve({ + port: 0, + fetch(req) { + req.headers.set("x-injected", "1"); + return new Response("ok"); + }, + }); + const headers = new Headers({ "x-probe": "alive" }); + const response = await server.fetch(`http://${server.hostname}:${server.port}/`, { headers }); + expect(await response.text()).toBe("ok"); + // The Request's header list must be a copy, so the handler's mutation is + // invisible here and the Request dropping must not free `headers`' + // C++ FetchHeaders (they used to share one refcount). + expect(headers.get("x-injected")).toBeNull(); + Bun.gc(true); + expect(headers.get("x-probe")).toBe("alive"); + }); + test("server should return a body for a OPTIONS Request", async () => { using server = Bun.serve({ port: 0, From 59f77ea07dd75c2e0e9094db4793ecb31e7a9f7b Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 10 Jul 2026 15:46:50 -0700 Subject: [PATCH 2/2] Extend ForeignRef to the rest of the owned C/C++ FFI handles (#33887) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacked on #33820, which introduces `ForeignRef` and converts `FetchHeaders`. ## What this does Extends the owned-handle pattern to every remaining opaque FFI type that Rust holds an ownership unit of, and generates the boilerplate instead of copying it. **1. Owned handles for 16 more types** — each replaces a hand-rolled owner, a `scopeguard`, or a bare raw pointer with a `#[repr(transparent)]` newtype whose `Drop` calls the C release function. Also flips 314 `&mut self` receivers to `&self` on `opaque_ffi!` ZSTs: those types are `UnsafeCell`-backed, so `&T` carries no `noalias` and C mutates through it — the `&mut` asserted an exclusivity that was never true and never needed. **2. `ForeignRef`** — a release-marker parameter. `ForeignOwned` admits one release per type, so an object with two ownership disciplines could not use it twice: libarchive's `struct archive` is freed by `archive_read_free` when opened for reading and `archive_write_free` when opened for writing, and the write side had degraded into a bespoke owner with a hand-written `Drop`. It is now `ForeignRef`. The parameter defaults, so no existing `ForeignRef` changed. **3. `foreign_handle!`** emits the newtype plus `adopt` / `adopt_ptr` / `as_ptr` / `leak` / `raw`. That block had been hand-copied onto 17 types; a missing `mem::forget` in one copy is a double-free the other sixteen would not reveal. Net -437 lines. `adopt` and `adopt_ptr` are now `unsafe`. Most hand-written copies were safe private fns, but adopting a pointer whose ownership unit you were not given is UB, so the obligation belongs at the call site — all 29 now carry a `SAFETY:` comment naming the producer. **4. The remaining C handles**, converted after reading the C and C++ rather than the names: `ENGINE`, `X509`, `X509_STORE`, `X509_STORE_CTX`, `SSL_SESSION`, `spng_ctx`, `WebPDemuxer`, `WebPMux`, and `CookieMapRef` folded onto `ForeignRef`. The certificate handles are careful about where the ref comes from. `SSL_get_peer_certificate`, `X509_up_ref`, `X509_STORE_CTX_get1_issuer` and `d2i_X509` hand over a `+1` and are adopted; `SSL_get_certificate`, `sk_X509_value` and `SSL_CTX_get_cert_store` return borrows and stay raw pointers. `SSL_set0_verify_cert_store` takes ownership, so that path leaks the handle rather than dropping it. ## Two bugs this surfaced - `cppbind` mapped `JSC::SourceProvider` to the owning handle, so the generated **safe** wrapper passed the address of a Rust stack slot to C++ `->deref()`. No caller today, but it also produced two conflicting `extern "C"` declarations of one symbol. - Flipping `Response::upgrade` to `&self` silently moved method resolution to `ResponseLike::upgrade`, which boxes its argument a second time. Rust probes the receiver by-value first, so an inherent `&mut self` method beats a trait method there; once flipped it no longer matches. Only an arity mismatch made it visible. Restores the debug-only corrupted-`HandleSlot` assert that `Strong::destroy` carried before it became a `ForeignRef`; a bad slot otherwise faults inside JSC with no Rust frame. ## Deliberately not converted | type | why | |---|---| | `AbortSignal`, `NapiEnv`, `JSCArrayBuffer` | already owned by `bun_ptr::ExternalShared`, which models ref/deref, not a single unit | | `Blob` (standalone_graph) | the opaque decl is an erased stand-in for a type declared a tier up | | `JSPropertyIteratorImpl` | freed by its enclosing struct's `Drop` | | `Channel` (c-ares) | see below | | `Loop`, `Heap`, `App` | thread/process-lifetime singletons | `ares_destroy()` invokes `query->callback(query->arg, ARES_EDESTRUCTION, 0, NULL)` for every pending query, and those callbacks re-enter `RefPtr::deref`. `Resolver` declares `ref_count` before `channel`, and Rust drops fields in declaration order, so a `Drop`-based owner would free the refcount's debug tables and then let the callbacks read them. The open-coded teardown in `Drop for GlobalData` is load-bearing. Converting it safely means declaring `channel` first, or keeping an explicit `drop(self.channel.take())`; left for a follow-up. `spng_ctx_free`, `WebPDemuxDelete` and `WebPMuxDelete` stay `unsafe` externs behind a plain wrapper: a `safe fn` taking `&sys::T` would let safe code free a context the handle owns. ## Verification - `cargo build -p bun_bin` clean. `cargo check` is not sufficient here: it stops before codegen, so it never evaluates the `const { assert!(size_of::() == 0) }` guards inside `opaque_deref*`, and a rename that turns `NonNull` from "the C object" into "an 8-byte Rust struct" typechecks fine. - `bun bd` clean, no warnings. - Drove each converted subsystem end to end: TLS handshake + peer certificate + session + the `rejectUnauthorized` reject path, PNG and WebP encode/decode round-trips (output byte-identical to before), cookie get/set through `Bun.serve` routes, `bun pm pack` and tarball extraction, `bun:ffi cc`, `--bytecode`, `.npmrc` regex, HMR WebSocket upgrade, `Bun.connect` failure path. Each hammered a few hundred iterations under `Bun.gc(true)` to check refcount balance. - Also adds a `verify` skill capturing the build-and-drive recipe. --- .claude/skills/verify/SKILL.md | 59 +++ Cargo.lock | 1 + src/boringssl/lib.rs | 35 +- src/boringssl_sys/boringssl.rs | 354 ++++++++++---- src/bundler_jsc/analyze_jsc.rs | 263 ++++++----- src/cares_sys/c_ares.rs | 47 +- src/codegen/cppbind.ts | 5 +- src/http/HTTPContext.rs | 5 +- src/http/HTTPThread.rs | 19 +- src/http/InternalState.rs | 6 +- src/http/compress_body.rs | 10 +- src/http/h3_client/ClientContext.rs | 22 +- src/http/h3_client/ClientSession.rs | 20 +- src/http/h3_client/PendingConnect.rs | 29 +- src/http/h3_client/Stream.rs | 12 +- src/http/h3_client/callbacks.rs | 17 +- src/http/h3_client/encode.rs | 4 +- src/http_jsc/websocket_client.rs | 4 +- .../websocket_client/WebSocketDeflate.rs | 8 +- .../WebSocketUpgradeClient.rs | 36 +- src/install/TarballStream.rs | 24 +- src/install/extract_tarball.rs | 2 +- src/install_types/Cargo.toml | 1 + src/install_types/NodeLinker.rs | 73 +-- src/jsc/CachedBytecode.rs | 98 ++-- src/jsc/CppTask.rs | 51 +- src/jsc/DOMFormData.rs | 43 +- src/jsc/DOMURL.rs | 21 +- src/jsc/Debugger.rs | 57 ++- src/jsc/FetchHeaders.rs | 84 +--- src/jsc/JSCScheduler.rs | 8 +- src/jsc/JSObject.rs | 27 +- src/jsc/JSPromise.rs | 14 +- src/jsc/JSSecrets.rs | 98 ++-- src/jsc/JSUint8Array.rs | 3 + src/jsc/MarkedArgumentBuffer.rs | 2 +- src/jsc/RegularExpression.rs | 143 +++--- src/jsc/SourceProvider.rs | 36 +- src/jsc/Strong.rs | 237 +++++----- src/jsc/TextCodec.rs | 72 +-- src/jsc/URL.rs | 106 +++-- src/jsc/URLSearchParams.rs | 10 +- src/jsc/VirtualMachine.rs | 2 +- src/jsc/Weak.rs | 139 +++--- src/jsc/ZigException.rs | 8 +- src/jsc/ZigStackTrace.rs | 11 +- src/jsc/array_buffer.rs | 4 +- src/jsc/bindgen.rs | 7 +- src/jsc/event_loop.rs | 15 +- src/jsc/lib.rs | 4 +- src/jsc/rare_data.rs | 35 +- src/jsc/virtual_machine_exports.rs | 7 +- src/libarchive/lib.rs | 447 ++++++++---------- src/libdeflate_sys/libdeflate.rs | 313 ++++++------ src/mimalloc_sys/mimalloc.rs | 24 +- src/opaque/lib.rs | 175 ++++++- src/options_types/context.rs | 7 +- src/runtime/api/Archive.rs | 16 +- src/runtime/api/BunObject.rs | 5 +- src/runtime/api/bun/SSLContextCache.rs | 34 +- src/runtime/api/bun/SecureContext.rs | 13 +- src/runtime/api/bun/x509.rs | 17 +- src/runtime/api/filesystem_router.rs | 2 +- src/runtime/bake/DevServer.rs | 5 +- src/runtime/cli/Arguments.rs | 4 +- src/runtime/cli/audit_command.rs | 2 +- src/runtime/cli/pack_command.rs | 2 +- src/runtime/cli/publish_command.rs | 2 +- src/runtime/cli/test_command.rs | 9 +- src/runtime/crypto/CryptoHasher.rs | 27 +- src/runtime/crypto/EVP.rs | 25 +- src/runtime/dispatch.rs | 12 +- src/runtime/dns_jsc/dns.rs | 6 +- src/runtime/ffi/ffi_body.rs | 22 +- src/runtime/image/codec_png.rs | 198 ++++---- src/runtime/image/codec_webp.rs | 107 +++-- src/runtime/ipc_host.rs | 4 +- src/runtime/jsc_hooks.rs | 5 +- src/runtime/node/zlib/NativeZstd.rs | 4 +- src/runtime/server/FileRoute.rs | 4 +- src/runtime/server/RequestContext.rs | 8 +- src/runtime/server/ServerConfig.rs | 2 +- src/runtime/server/StaticRoute.rs | 2 +- src/runtime/server/mod.rs | 53 +-- src/runtime/server/server_body.rs | 36 +- src/runtime/socket/Listener.rs | 60 +-- src/runtime/socket/SSLConfig.rs | 8 +- src/runtime/socket/SocketAddress.rs | 9 +- src/runtime/socket/UpgradedDuplex.rs | 5 +- src/runtime/socket/WindowsNamedPipe.rs | 23 +- src/runtime/socket/WindowsNamedPipeContext.rs | 4 +- src/runtime/socket/socket_body.rs | 37 +- src/runtime/socket/tls_socket_functions.rs | 266 +++++++---- src/runtime/socket/udp_socket.rs | 70 +-- src/runtime/socket/uws_dispatch.rs | 4 +- src/runtime/test_runner/ScopeFunctions.rs | 7 +- src/runtime/test_runner/jest.rs | 12 +- src/runtime/valkey_jsc/js_valkey.rs | 23 +- src/runtime/webcore/Blob.rs | 18 +- src/runtime/webcore/Body.rs | 6 +- src/runtime/webcore/CookieMap.rs | 70 ++- src/runtime/webcore/Crypto.rs | 9 +- src/runtime/webcore/FormData.rs | 2 +- src/runtime/webcore/TextDecoder.rs | 40 +- src/runtime/webcore/blob/copy_file.rs | 10 +- src/runtime/webcore/streams.rs | 30 +- src/sha_hmac/sha.rs | 8 +- src/sql_jsc/mysql/MySQLConnection.rs | 4 +- src/sql_jsc/postgres/PostgresSQLConnection.rs | 3 +- src/sql_jsc/shared/ConnectionCtorArgs.rs | 4 +- src/tcc_sys/tcc.rs | 133 +++--- src/uws/lib.rs | 43 +- src/uws_sys/App.rs | 226 +++++---- src/uws_sys/ConnectingSocket.rs | 75 ++- src/uws_sys/ListenSocket.rs | 60 +-- src/uws_sys/Request.rs | 6 +- src/uws_sys/Response.rs | 112 +++-- src/uws_sys/SocketContext.rs | 2 +- src/uws_sys/WebSocket.rs | 10 +- src/uws_sys/h3.rs | 237 +++++----- src/uws_sys/lib.rs | 5 +- src/uws_sys/quic/Context.rs | 56 +-- src/uws_sys/quic/PendingConnect.rs | 24 +- src/uws_sys/quic/Stream.rs | 59 ++- src/uws_sys/socket.rs | 6 +- src/uws_sys/udp.rs | 67 +-- src/uws_sys/us_socket_t.rs | 133 +++--- src/zstd/lib.rs | 149 +++--- test/internal/dead-code-escape-limits.json | 2 +- 129 files changed, 3311 insertions(+), 2816 deletions(-) create mode 100644 .claude/skills/verify/SKILL.md diff --git a/.claude/skills/verify/SKILL.md b/.claude/skills/verify/SKILL.md new file mode 100644 index 000000000000..7c8533ad8a8c --- /dev/null +++ b/.claude/skills/verify/SKILL.md @@ -0,0 +1,59 @@ +--- +name: verify +description: Build Bun and drive the changed code at its real surface (CLI, socket, FFI) to observe it running. +--- + +# Verifying a change to Bun + +**Build:** `bun bd` (no timeout — it can take many minutes). Exit 0 is setup, not evidence. + +**Drive:** `bun bd run ` builds *and* runs, forwarding args to the debug binary. +Put driver scripts under `~/code/tmp/**` — Santa blocks unsigned executables elsewhere. + +## Two ways to invoke the debug build + +| Need | Use | +|---|---| +| run a script, stay in the repo | `bun bd run /path/to/drive.js` | +| any command, from another cwd | `/Users/jarred/code/bun/build/debug/bun-debug ` | + +`bun bd` is a **package.json script** — it only resolves with the repo root as cwd. +A probe that `cd`s into a temp dir must call the binary by absolute path. +The binary refuses `bun-debug test ` on purpose ("use `bun bd test`"); every other +subcommand (`pm pack`, `install`, `build`, `run`) works directly. Directory args to +`bun-debug test` also trip a filter guard — pass explicit file paths. + +## Surfaces, by what you touched + +| Changed | Drive it with | +|---|---| +| `src/uws_sys/**`, `src/runtime/server/**` | `Bun.serve({port:0})` + real `fetch()`; `routes:` for static routes | +| WebSocket / `Response::upgrade` | `Bun.serve` + `new WebSocket(...)`, echo a `Uint8Array` | +| TLS / `SSL_CTX` | `Bun.serve({tls:{cert,key}})` + `fetch(https, {tls:{rejectUnauthorized:false}})`. Make a cert with `openssl req -x509 -newkey rsa:2048 -nodes -subj /CN=localhost -addext subjectAltName=DNS:localhost` | +| `ConnectingSocket` (connect-failure path) | `Bun.connect()` to a port you opened then closed → `connectError` fires | +| `src/runtime/bake/**` (dev server) | run `bun-debug index.html --port 0`, read the URL off stdout, then open `ws://host:port/_bun/hmr` | +| `libdeflate`, `zstd`, `node:zlib` | `Bun.gzipSync`/`gunzipSync`, `Bun.zstdCompressSync`, `zlib.brotliCompress`. Feed garbage in too — it must throw, not crash | +| `libarchive` | write side = `bun-debug pm pack`; read side = `bun-debug install ./x.tgz --no-save`, then check the extracted file exists | +| `src/jsc/CachedBytecode.rs` | `bun-debug build x.js --bytecode --target=bun --outdir=out` then run `out/x.js` | +| `src/tcc_sys/**` | `import { cc } from "bun:ffi"` and call a compiled C symbol | +| Yarr `RegularExpression` | `.npmrc` with `public-hoist-pattern[]=*x*`, then `bun-debug install --dry-run` | +| `TextCodec` | `TextDecoder`, including `{stream:true}` across a split multi-byte codepoint | +| `JSUint8Array` | `crypto.getRandomValues(new Uint8Array(n))` (DOMJIT fast path); `ws.send(bytes)` | +| `SourceProvider` | `new Error().stack` must contain `file:line` | +| `Strong` / `Weak` | `WeakRef` + `Bun.gc(true)`; churn thousands of promises | + +## Gotchas that cost real time + +- **A debug assert you add is only real if it's in the binary**: `strings build/debug/bun-debug | rg ''`. +- **`cargo check` is not an oracle.** It never monomorphizes, so it never evaluates + `const { assert!(...) }` inside a generic fn (`bun_opaque::opaque_deref*`). Finish with + `cargo build -p bun_bin` or `bun bd`. +- **Generated code is built by ninja, not cargo.** `build/debug/codegen/*.rs` goes stale under + a bare `cargo check`. Regenerate a single file with e.g. + `bun src/codegen/generate-host-exports.ts build/debug/codegen`, or just run `bun bd`. +- **Multi-file test runs share one process.** RSS/GC assertions (`gcUntilCountAtMost`, + "does not leak memory") and tests that mutate process globals (`buffer.kMaxLength`) fail + when run alongside other files even with `--isolate`. Re-run the file alone before believing it. +- **Compare against a baseline binary, not intuition.** `~/code/bun-3` tracks `main` and usually + has a built `build/debug/bun-debug`. Run the same file with it to tell a regression from a + pre-existing flake. diff --git a/Cargo.lock b/Cargo.lock index 6fa3964e716e..0efa64f78df5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1025,6 +1025,7 @@ dependencies = [ "bun_ast", "bun_collections", "bun_core", + "bun_opaque", "bun_semver", "bun_wyhash", "const_format", diff --git a/src/boringssl/lib.rs b/src/boringssl/lib.rs index e3f6239b63e1..b3b6f9dee09c 100644 --- a/src/boringssl/lib.rs +++ b/src/boringssl/lib.rs @@ -99,7 +99,7 @@ type SslCustomVerifyCb = unsafe extern "C" { fn SSL_CTX_set_custom_verify( - ctx: *mut boring::SSL_CTX, + ctx: *mut boring::sys::SSL_CTX, mode: c_int, callback: SslCustomVerifyCb, ); @@ -114,7 +114,7 @@ unsafe extern "C" fn noop_custom_verify( /// `Send + Sync` newtype around the process-lifetime client `SSL_CTX*` so it /// can sit inside a `OnceLock` (raw pointers opt out of `Send`/`Sync`). -struct CtxStore(ptr::NonNull); +struct CtxStore(ptr::NonNull); // SAFETY: `SSL_CTX` is internally thread-safe per BoringSSL docs (its refcount // and method tables are guarded by `CRYPTO_MUTEX`); we only ever bump the // refcount and hand it to `SSL_new`, both of which BoringSSL documents as @@ -138,11 +138,12 @@ std::thread_local! { /// /// # Safety /// `ctx` must be a live `SSL_CTX*`. -pub unsafe fn ssl_ctx_setup(ctx: *mut boring::SSL_CTX) { +pub unsafe fn ssl_ctx_setup(ctx: *mut boring::sys::SSL_CTX) { + let ctx = boring::sys::SSL_CTX::opaque_ref(ctx); AUTO_CRYPTO_BUFFER_POOL.with(|pool| { - // SAFETY: caller guarantees `ctx` is a live `SSL_CTX*`; the pool pointer - // is either freshly returned by `CRYPTO_BUFFER_POOL_new` or a previously - // stored thread-local pool, and `SSL_DEFAULT_CIPHER_LIST` is a valid C string. + // SAFETY: the pool pointer is either freshly returned by + // `CRYPTO_BUFFER_POOL_new` or a previously stored thread-local pool, and + // `SSL_DEFAULT_CIPHER_LIST` is a valid C string. unsafe { if pool.get().is_null() { pool.set(CRYPTO_BUFFER_POOL_new()); @@ -159,7 +160,7 @@ pub fn init_client() -> *mut boring::SSL { // Bump the refcount on every call after the first; the first call's // `SSL_CTX_new` already returns refcount = 1. if let Some(stored) = CTX_STORE.get() { - let _ = boring::SSL_CTX_up_ref(stored.0.as_ptr()); + let _ = boring::SSL_CTX_up_ref(boring::sys::SSL_CTX::opaque_ref(stored.0.as_ptr())); } let ctx = CTX_STORE .get_or_init(|| { @@ -167,15 +168,17 @@ pub fn init_client() -> *mut boring::SSL { // 1. SSL_CTX_new(TLS_with_buffers_method()) // 2. setCustomVerify(noop_custom_verify) → SSL_CTX_set_custom_verify(ctx, 0, cb) // 3. setup() → CRYPTO_BUFFER_POOL_new + set0_buffer_pool + set_cipher_list("ALL") - let ctx = boring::SSL_CTX_new(boring::TLS_with_buffers_method()); - SSL_CTX_set_custom_verify(ctx, 0, Some(noop_custom_verify)); - ssl_ctx_setup(ctx); - CtxStore(ptr::NonNull::new(ctx).expect("SSL_CTX_new")) + let ctx = + boring::SSL_CTX::new(boring::TLS_with_buffers_method()).expect("SSL_CTX_new"); + SSL_CTX_set_custom_verify(ctx.as_ptr(), 0, Some(noop_custom_verify)); + ssl_ctx_setup(ctx.as_ptr()); + // Process-lifetime: this +1 is never given back. + CtxStore(ctx.leak()) }) .0 .as_ptr(); - let ssl = boring::SSL_new(ctx); + let ssl = boring::SSL_new(boring::sys::SSL_CTX::opaque_ref(ctx)); boring::SSL_set_connect_state(ssl); ssl @@ -323,7 +326,7 @@ fn match_dns_name(pattern: &[u8], hostname: &[u8]) -> bool { strings::eql_case_insensitive_ascii(pattern, hostname, true) } -pub fn check_x509_server_identity(x509: &mut boring::X509, hostname: &[u8]) -> bool { +pub fn check_x509_server_identity(x509: &mut boring::sys::X509, hostname: &[u8]) -> bool { let host_is_ip = strings::is_ip_address(hostname); // Node.js: CN is consulted only when the certificate carries no // DNS / IP / URI subjectAltName entries. Track whether any were seen. @@ -333,7 +336,7 @@ pub fn check_x509_server_identity(x509: &mut boring::X509, hostname: &[u8]) -> b // SAFETY: x509 is a valid &mut so non-null/aligned; all boring:: fns are // null-safe where documented. unsafe { - let x509: *mut boring::X509 = x509; + let x509: *mut boring::sys::X509 = x509; let index = boring::X509_get_ext_by_NID(x509, boring::NID_subject_alt_name, -1); if index >= 0 { // we can check hostname @@ -353,7 +356,9 @@ pub fn check_x509_server_identity(x509: &mut boring::X509, hostname: &[u8]) -> b None }; - if let Some(names) = boring::GeneralNames::from_raw(boring::X509V3_EXT_d2i(ext)) { + if let Some(names) = + boring::struct_stack_st_GENERAL_NAME::from_raw(boring::X509V3_EXT_d2i(ext)) + { for name in names.iter() { match name.name_type { boring::GEN_URI => { diff --git a/src/boringssl_sys/boringssl.rs b/src/boringssl_sys/boringssl.rs index b5ce222206a5..793288f45c95 100644 --- a/src/boringssl_sys/boringssl.rs +++ b/src/boringssl_sys/boringssl.rs @@ -61,10 +61,6 @@ pub(crate) type ASN1_IA5STRING = asn1_string_st; // Opaque handles // ═══════════════════════════════════════════════════════════════════════════ -opaque!( - /// `struct engine_st` (`typedef ... ENGINE`). - ENGINE -); opaque!( /// `struct env_md_st` (`typedef ... EVP_MD`). EVP_MD @@ -73,18 +69,45 @@ opaque!( /// `struct ssl_st` (`typedef ... SSL`). SSL ); -opaque!( - /// `struct ssl_ctx_st` (`typedef ... SSL_CTX`). - SSL_CTX -); +/// The C objects themselves. Only the extern declarations name these types; +/// all Rust code uses the owning handles ([`SSL_CTX`], [`ENGINE`], [`X509`], +/// [`X509_STORE`], [`X509_STORE_CTX`], [`struct_stack_st_GENERAL_NAME`]) that +/// wrap them. +pub mod sys { + ::bun_opaque::opaque_ffi! { + /// `struct ssl_ctx_st` (`typedef ... SSL_CTX`). `&Self` is ABI-identical + /// to a non-null `SSL_CTX*` and carries no `noalias`/`readonly` — + /// BoringSSL mutates the context (refcount, session cache) through it. + pub struct SSL_CTX; + } + ::bun_opaque::opaque_ffi! { + /// `STACK_OF(GENERAL_NAME)`. `&Self` is ABI-identical to a non-null + /// `OPENSSL_STACK*` and carries no `noalias`/`readonly` — BoringSSL + /// mutates the stack's bookkeeping through it. + pub struct struct_stack_st_GENERAL_NAME; + } + ::bun_opaque::opaque_ffi! { + /// `struct engine_st` (`typedef ... ENGINE`). `&Self` is ABI-identical + /// to a non-null `ENGINE*` and carries no `noalias`/`readonly`. + pub struct ENGINE; + /// `struct x509_st` (`typedef ... X509`). `&Self` is ABI-identical to a + /// non-null `X509*` and carries no `noalias`/`readonly` — BoringSSL + /// mutates the refcount and the cached extension fields through it. + pub struct X509; + /// `struct x509_store_st` (`typedef ... X509_STORE`). `&Self` is + /// ABI-identical to a non-null `X509_STORE*` and carries no + /// `noalias`/`readonly` — BoringSSL mutates the refcount through it. + pub struct X509_STORE; + /// `struct x509_store_ctx_st` (`typedef ... X509_STORE_CTX`). `&Self` is + /// ABI-identical to a non-null `X509_STORE_CTX*` and carries no + /// `noalias`/`readonly` — BoringSSL mutates the lookup state through it. + pub struct X509_STORE_CTX; + } +} opaque!( /// `struct crypto_buffer_pool_st` (`typedef ... CRYPTO_BUFFER_POOL`). CRYPTO_BUFFER_POOL ); -opaque!( - /// `struct x509_st` (`typedef ... X509`). - X509 -); opaque!( /// `struct X509_name_st` (`typedef ... X509_NAME`). X509_NAME @@ -121,10 +144,6 @@ opaque!( /// `STACK_OF(X509)` — opaque stack handle. struct_stack_st_X509 ); -opaque!( - /// `STACK_OF(GENERAL_NAME)` — opaque stack handle. - struct_stack_st_GENERAL_NAME -); opaque!( /// `struct crypto_ex_data_st` (`typedef ... CRYPTO_EX_DATA`). CRYPTO_EX_DATA @@ -284,52 +303,194 @@ unsafe extern "C" { fn GENERAL_NAME_free(name: *mut GENERAL_NAME); } -/// Owns one `SSL_CTX` reference; `SSL_CTX_free`s it on drop. Construct from a -/// pointer that already carries a +1 (`SSL_CTX_new`, `SSL_CTX_up_ref`). -pub struct OwnedSslCtx(core::ptr::NonNull); +// `SSL_CTX_new` / `SSL_CTX_up_ref` hand back a `+1` on the context's +// `CRYPTO_refcount_t`. One `SSL_CTX` handle owns exactly that one ref. +bun_opaque::foreign_handle! { + /// Owned handle to a BoringSSL `SSL_CTX`. + /// + /// Holds one ref on the C refcount; `Drop` gives it back. Every method takes + /// `&self`: the context is shared with every `SSL` created from it, and + /// BoringSSL mutates it (refcount, session cache) through the same pointer. + /// + /// A context borrowed from C (`SSL_get_SSL_CTX`, the weak `SSLContextCache` + /// slot) took no ref and stays a raw `*mut sys::SSL_CTX`. + pub struct SSL_CTX(sys::SSL_CTX) via SSL_CTX_free; +} -impl OwnedSslCtx { - /// Takes the +1 `raw` carries; `None` when `raw` is null. +impl SSL_CTX { + /// `SSL_CTX_new` returns a fresh `+1`; `None` on allocation failure. /// /// # Safety - /// `raw` must be null or carry a reference the caller is giving up. - pub unsafe fn from_raw(raw: *mut SSL_CTX) -> Option { - core::ptr::NonNull::new(raw).map(Self) + /// `method` must be a live `SSL_METHOD*`. + pub unsafe fn new(method: *const SSL_METHOD) -> Option { + // SAFETY: caller contract; the result carries a fresh +1. + unsafe { Self::adopt_ptr(SSL_CTX_new(method)) } } - pub fn as_ptr(&self) -> *mut SSL_CTX { - self.0.as_ptr() + /// Take a second ref on the same context. + #[inline] + pub fn up_ref(&self) -> Self { + let _ = SSL_CTX_up_ref(self.raw()); + // SAFETY: the call above added exactly the ref this handle takes. + unsafe { Self::adopt(self.0.as_non_null()) } } +} + +// ═══════════════════════════════════════════════════════════════════════════ +// Owned handles — ENGINE / X509 / X509_STORE / X509_STORE_CTX +// ═══════════════════════════════════════════════════════════════════════════ + +/// `ENGINE_free` takes `*mut ENGINE` and returns `int`; `foreign_handle!` needs +/// a `fn(&sys::ENGINE)`. +fn engine_free_release(engine: &sys::ENGINE) { + // SAFETY: the handle owns the sole allocation `ENGINE_new` returned; + // `as_mut_ptr` derives write provenance from the `UnsafeCell` body. The + // `int` result is BoringSSL's OpenSSL-compat "always 1". + let _ = unsafe { ENGINE_free(engine.as_mut_ptr()) }; +} + +// `ENGINE_new` allocates and hands back the sole owner (no refcount). +bun_opaque::foreign_handle! { + /// Owned handle to a BoringSSL `ENGINE`. + /// + /// Holds the one allocation `ENGINE_new` returned; `Drop` frees it. Every + /// method takes `&self`: BoringSSL mutates the engine through the same + /// pointer, so there is no `&mut self` to have. + /// + /// Every consumer (`EVP_DigestInit_ex`, `EVP_Digest`, `HMAC_Init_ex`) only + /// *borrows* the engine and takes a bare `*mut sys::ENGINE`, so + /// [`Self::as_ptr`] is the accessor those call sites want. + pub struct ENGINE(sys::ENGINE) via engine_free_release; +} - /// Transfers the reference back out; the caller must free it. - pub fn into_raw(self) -> *mut SSL_CTX { - core::mem::ManuallyDrop::new(self).0.as_ptr() +impl ENGINE { + /// `ENGINE_new` allocates a fresh engine; `None` on allocation failure. + pub fn new() -> Option { + // SAFETY: `ENGINE_new` returns a fresh, solely-owned `ENGINE*`, or null + // on OOM (having allocated nothing). No other handle gives that unit back. + unsafe { Self::adopt_ptr(ENGINE_new()) } } } -impl Drop for OwnedSslCtx { - fn drop(&mut self) { - // SAFETY: we own exactly one reference, released once. - unsafe { SSL_CTX_free(self.0.as_ptr()) } +/// `X509_free` takes `*mut X509`; `foreign_handle!` needs a `fn(&sys::X509)`. +fn x509_free_release(x509: &sys::X509) { + // SAFETY: the handle owns one ref on the certificate's `CRYPTO_refcount_t`; + // `X509_free` gives back exactly that one. + unsafe { X509_free(x509.as_mut_ptr()) } +} + +// `d2i_X509`, `SSL_get_peer_certificate`, `X509_up_ref` and +// `X509_STORE_CTX_get1_issuer` each hand back a `+1` on the certificate's +// refcount. One `X509` handle owns exactly that one ref. +bun_opaque::foreign_handle! { + /// Owned handle to a BoringSSL `X509` certificate. + /// + /// Holds one ref on the C refcount; `Drop` gives it back. Every method takes + /// `&self`: a refcount is shared by definition, and BoringSSL mutates the + /// certificate's cached fields through the same pointer. + /// + /// A certificate *borrowed* from a chain (`sk_X509_value`) or from the local + /// connection (`SSL_get_certificate`) took no ref and stays a raw + /// `*mut sys::X509`. + pub struct X509(sys::X509) via x509_free_release; +} + +/// `X509_STORE_free` takes `*mut X509_STORE`. +fn x509_store_free_release(store: &sys::X509_STORE) { + // SAFETY: the handle owns one ref on the store's refcount; `X509_STORE_free` + // gives back exactly that one. + unsafe { X509_STORE_free(store.as_mut_ptr()) } +} + +// `us_get_shared_default_ca_store` up-refs the process-wide store before +// returning; one handle owns exactly that one ref. +bun_opaque::foreign_handle! { + /// Owned handle to a BoringSSL `X509_STORE`. + /// + /// Holds one ref on the C refcount; `Drop` gives it back. Every method takes + /// `&self`: the store is shared with every context that references it. + /// + /// A store *borrowed* from a context (`SSL_CTX_get_cert_store`) took no ref + /// and stays a raw `*mut sys::X509_STORE`. A store handed to a `set0` sink + /// (`SSL_set0_verify_cert_store`) is transferred, not released. + pub struct X509_STORE(sys::X509_STORE) via x509_store_free_release; +} + +/// `X509_STORE_CTX_free` takes `*mut X509_STORE_CTX`. +fn x509_store_ctx_free_release(ctx: &sys::X509_STORE_CTX) { + // SAFETY: the handle owns the sole allocation `X509_STORE_CTX_new` returned. + unsafe { X509_STORE_CTX_free(ctx.as_mut_ptr()) } +} + +// `X509_STORE_CTX_new` allocates and hands back the sole owner (no refcount). +bun_opaque::foreign_handle! { + /// Owned handle to a BoringSSL `X509_STORE_CTX`. + /// + /// Holds the one allocation `X509_STORE_CTX_new` returned; `Drop` frees it. + /// Every method takes `&self`: BoringSSL mutates the lookup state through + /// the same pointer, so there is no `&mut self` to have. + /// + /// `X509_STORE_CTX_init` only *borrows* the store and the leaf it is given. + pub struct X509_STORE_CTX(sys::X509_STORE_CTX) via x509_store_ctx_free_release; +} + +// `X509V3_EXT_d2i` allocates the stack and every `GENERAL_NAME` in it, then +// hands back the sole owner. One handle owns exactly that one allocation. +// The `foreign_owned!` impl is emitted by the `foreign_handle!` invocation below. + +/// Frees every `GENERAL_NAME` element, then the stack itself. +fn general_name_stack_free(sk: &sys::struct_stack_st_GENERAL_NAME) { + // SAFETY: `sk_pop_free_ex` invokes the callback once per element, so it + // gets `GENERAL_NAME_free` (per element), not a stack free. `as_mut_ptr` + // derives write provenance from the handle's `UnsafeCell` body. + unsafe { + sk_pop_free_ex( + sk.as_mut_ptr().cast::(), + Some(call_general_name_free), + Some(core::mem::transmute::< + unsafe extern "C" fn(*mut GENERAL_NAME), + unsafe extern "C" fn(*mut c_void), + >(GENERAL_NAME_free)), + ) } } -/// Owns the `STACK_OF(GENERAL_NAME)` that `X509V3_EXT_d2i` returns for a -/// subjectAltName extension. Frees every `GENERAL_NAME` and then the stack. -pub struct GeneralNames(core::ptr::NonNull); +bun_opaque::foreign_handle! { + /// Owned handle to the `STACK_OF(GENERAL_NAME)` that `X509V3_EXT_d2i` returns + /// for a subjectAltName extension. + /// + /// Holds the one allocation BoringSSL handed us; `Drop` frees the elements and + /// then the stack. Every method takes `&self`: BoringSSL mutates the stack's + /// bookkeeping through the same pointer, so there is no `&mut self` to have. + pub struct struct_stack_st_GENERAL_NAME(sys::struct_stack_st_GENERAL_NAME) via general_name_stack_free; +} -impl GeneralNames { +/// Ownership plumbing the macro does not emit. +impl struct_stack_st_GENERAL_NAME { /// Takes ownership of a `STACK_OF(GENERAL_NAME)`; `None` when `raw` is null. /// + /// Not [`Self::adopt_ptr`]: this takes the untyped `*mut c_void` that + /// `X509V3_EXT_d2i` returns and casts it here. + /// /// # Safety /// `raw` must be null or a stack the caller owns and does not free itself. pub unsafe fn from_raw(raw: *mut c_void) -> Option { - core::ptr::NonNull::new(raw.cast::()).map(Self) + // SAFETY: caller contract. + core::ptr::NonNull::new(raw.cast::()) + .map(|p| unsafe { Self::adopt(p) }) } + /// The untyped view every `sk_*` entry point takes. + fn stack(&self) -> *mut OPENSSL_STACK { + self.raw().as_mut_ptr().cast::() + } +} + +/// Element access. `&self` throughout; BoringSSL owns the elements until `Drop`. +impl struct_stack_st_GENERAL_NAME { pub fn len(&self) -> usize { // SAFETY: we own a live stack; `sk_num` takes it as `const OPENSSL_STACK`. - unsafe { sk_num(self.0.as_ptr().cast::()) } + unsafe { sk_num(self.stack()) } } pub fn is_empty(&self) -> bool { @@ -343,11 +504,7 @@ impl GeneralNames { } // SAFETY: `i` is in bounds and the stack outlives the borrow, which is // tied to `&self`. BoringSSL owns the element until our `Drop`. - unsafe { - sk_value(self.0.as_ptr().cast::(), i) - .cast::() - .as_ref() - } + unsafe { sk_value(self.stack(), i).cast::().as_ref() } } pub fn iter(&self) -> impl Iterator { @@ -355,27 +512,11 @@ impl GeneralNames { } } -impl Drop for GeneralNames { - fn drop(&mut self) { - // SAFETY: `sk_pop_free_ex` invokes the callback once per element, so it - // gets `GENERAL_NAME_free` (per element), not a stack free. - unsafe { - sk_pop_free_ex( - self.0.as_ptr().cast::(), - Some(call_general_name_free), - Some(core::mem::transmute::< - unsafe extern "C" fn(*mut GENERAL_NAME), - unsafe extern "C" fn(*mut c_void), - >(GENERAL_NAME_free)), - ) - } - } -} - /// Restores the element type erased through `OPENSSL_sk_free_func`. unsafe extern "C" fn call_general_name_free(free_func: OPENSSL_sk_free_func, ptr: *mut c_void) { - // SAFETY: `free_func` is `GENERAL_NAME_free` erased in `Drop` above; both - // sides are `extern "C" fn(*mut _)`, so the round-trip is ABI-sound. + // SAFETY: `free_func` is `GENERAL_NAME_free` erased in + // `general_name_stack_free` above; both sides are `extern "C" fn(*mut _)`, + // so the round-trip is ABI-sound. let f: unsafe extern "C" fn(*mut GENERAL_NAME) = unsafe { core::mem::transmute(free_func.expect("non-null free_func")) }; // SAFETY: `ptr` is an element `sk_pop_free_ex` is draining from the stack. @@ -439,7 +580,7 @@ unsafe extern "C" { pub fn EVP_DigestInit_ex( ctx: *mut EVP_MD_CTX, type_: *const EVP_MD, - engine: *mut ENGINE, + engine: *mut sys::ENGINE, ) -> c_int; pub fn EVP_DigestUpdate(ctx: *mut EVP_MD_CTX, data: *const c_void, len: usize) -> c_int; pub fn EVP_DigestFinal(ctx: *mut EVP_MD_CTX, md_out: *mut u8, out_size: *mut c_uint) -> c_int; @@ -459,7 +600,7 @@ unsafe extern "C" { md_out: *mut u8, md_out_size: *mut c_uint, type_: *const EVP_MD, - impl_: *mut ENGINE, + impl_: *mut sys::ENGINE, ) -> c_int; // ── HMAC ───────────────────────────────────────────────────────────── @@ -515,16 +656,24 @@ unsafe extern "C" { // ── SSL ────────────────────────────────────────────────────────────── pub safe fn SSL_library_init() -> c_int; pub safe fn SSL_load_error_strings(); - pub fn SSL_CTX_up_ref(ctx: *mut SSL_CTX) -> c_int; + pub safe fn SSL_CTX_up_ref(ctx: &sys::SSL_CTX) -> c_int; pub fn SSL_get_peer_cert_chain(ssl: *const SSL) -> *mut struct_stack_st_X509; // ── X509 ───────────────────────────────────────────────────────────── - pub fn d2i_X509(out: *mut *mut X509, inp: *mut *const u8, len: c_long) -> *mut X509; - pub fn i2d_X509(x: *mut X509, outp: *mut *mut u8) -> c_int; - pub fn X509_free(x509: *mut X509); - pub fn X509_get_subject_name(x509: *const X509) -> *mut X509_NAME; - pub fn X509_get_ext_by_NID(x: *const X509, nid: c_int, lastpos: c_int) -> c_int; - pub fn X509_get_ext(x: *const X509, loc: c_int) -> *mut X509_EXTENSION; + pub fn d2i_X509(out: *mut *mut sys::X509, inp: *mut *const u8, len: c_long) -> *mut sys::X509; + pub fn i2d_X509(x: *mut sys::X509, outp: *mut *mut u8) -> c_int; + /// Decrements the certificate's refcount; frees it at zero. Backs the + /// [`X509`] handle's `Drop`. + pub fn X509_free(x509: *mut sys::X509); + /// Decrements the store's refcount; frees it at zero. Backs the + /// [`X509_STORE`] handle's `Drop`. + pub fn X509_STORE_free(store: *mut sys::X509_STORE); + /// Frees the lookup context `X509_STORE_CTX_new` allocated. Backs the + /// [`X509_STORE_CTX`] handle's `Drop`. + pub fn X509_STORE_CTX_free(ctx: *mut sys::X509_STORE_CTX); + pub fn X509_get_subject_name(x509: *const sys::X509) -> *mut X509_NAME; + pub fn X509_get_ext_by_NID(x: *const sys::X509, nid: c_int, lastpos: c_int) -> c_int; + pub fn X509_get_ext(x: *const sys::X509, loc: c_int) -> *mut X509_EXTENSION; pub fn X509_NAME_get_index_by_NID(name: *const X509_NAME, nid: c_int, lastpos: c_int) -> c_int; pub fn X509_NAME_get_entry(name: *const X509_NAME, loc: c_int) -> *mut X509_NAME_ENTRY; pub fn X509_NAME_ENTRY_get_data(entry: *const X509_NAME_ENTRY) -> *mut ASN1_STRING; @@ -540,14 +689,16 @@ unsafe extern "C" { // symbol — they bottom out on the untyped `sk_*` ABI above. // ═══════════════════════════════════════════════════════════════════════════ +/// Borrows the `i`th element; the stack keeps the ref, so the result names the +/// C object (`sys::X509`), never the owning [`X509`] handle. #[inline] -pub unsafe fn sk_X509_value(sk: *const struct_stack_st_X509, i: usize) -> *mut X509 { +pub unsafe fn sk_X509_value(sk: *const struct_stack_st_X509, i: usize) -> *mut sys::X509 { // SAFETY: Two independent type casts, not a const→mut provenance laundering: // - `sk` is reinterpreted `*const opaque -> *const OPENSSL_STACK` (const→const). // - `sk_value` returns `*mut c_void` from the C heap; we narrow that to - // `*mut X509` (mut→mut). Mutability originates from BoringSSL's ABI + // `*mut sys::X509` (mut→mut). Mutability originates from BoringSSL's ABI // (`void *sk_value(const _STACK *, size_t)`), not from `sk`. - unsafe { sk_value(sk.cast::(), i).cast::() } + unsafe { sk_value(sk.cast::(), i).cast::() } } // ═══════════════════════════════════════════════════════════════════════════ @@ -646,22 +797,15 @@ opaque!( /// `struct ssl_method_st` (`typedef ... SSL_METHOD`). SSL_METHOD ); -opaque!( - /// `struct x509_store_st` (`typedef ... X509_STORE`). - X509_STORE -); -opaque!( - /// `struct x509_store_ctx_st` (`typedef ... X509_STORE_CTX`). - X509_STORE_CTX -); opaque!( /// `struct rsa_st` (`typedef ... RSA`). RSA ); /// `int (*SSL_verify_cb)(int preverify_ok, X509_STORE_CTX *ctx)` — verify -/// callback type for `SSL_set_verify` / `SSL_CTX_set_verify`. -pub type SSL_verify_cb = Option c_int>; +/// callback type for `SSL_set_verify` / `SSL_CTX_set_verify`. BoringSSL owns the +/// context it passes, so the callback sees the C object, not the owning handle. +pub type SSL_verify_cb = Option c_int>; /// `int pem_password_cb(char *buf, int size, int rwflag, void *userdata)`. pub(crate) type pem_password_cb = @@ -676,23 +820,32 @@ unsafe extern "C" { pub safe fn TLS_with_buffers_method() -> *const SSL_METHOD; // ── ENGINE ─────────────────────────────────────────────────────────── - pub safe fn ENGINE_new() -> *mut ENGINE; - pub fn ENGINE_free(engine: *mut ENGINE) -> c_int; + /// Allocates a fresh engine; wrapped by [`ENGINE::new`]. + pub safe fn ENGINE_new() -> *mut sys::ENGINE; + /// Frees the engine. Backs the [`ENGINE`] handle's `Drop`. + pub fn ENGINE_free(engine: *mut sys::ENGINE) -> c_int; // ── SSL_CTX ────────────────────────────────────────────────────────── - pub fn SSL_CTX_new(method: *const SSL_METHOD) -> *mut SSL_CTX; - pub fn SSL_CTX_free(ctx: *mut SSL_CTX); - pub fn SSL_CTX_get_verify_mode(ctx: *const SSL_CTX) -> c_int; - pub fn SSL_CTX_set_ex_data(ctx: *mut SSL_CTX, idx: c_int, data: *mut c_void) -> c_int; - pub fn SSL_CTX_get_ex_data(ctx: *const SSL_CTX, idx: c_int) -> *mut c_void; - pub fn SSL_CTX_set0_buffer_pool(ctx: *mut SSL_CTX, pool: *mut CRYPTO_BUFFER_POOL); - pub fn SSL_CTX_set_cipher_list(ctx: *mut SSL_CTX, str_: *const c_char) -> c_int; + // `&sys::SSL_CTX` is a non-null `SSL_CTX*` carrying no `noalias`/`readonly`, + // so shims taking only it plus scalars are `safe fn`. The rest keep an + // `unsafe fn` body: C dereferences the raw pointer they also carry. + pub fn SSL_CTX_new(method: *const SSL_METHOD) -> *mut sys::SSL_CTX; + // safe: decrements the intrusive refcount. A decrement is not exclusive + // access — other refs exist by definition — so the receiver is `&`. + pub safe fn SSL_CTX_free(ctx: &sys::SSL_CTX); + pub safe fn SSL_CTX_get_verify_mode(ctx: &sys::SSL_CTX) -> c_int; + // NOT `safe fn`: `data` is stashed and later dereferenced by the + // `CRYPTO_EX_free` callback registered for the slot. + pub fn SSL_CTX_set_ex_data(ctx: &sys::SSL_CTX, idx: c_int, data: *mut c_void) -> c_int; + pub safe fn SSL_CTX_get_ex_data(ctx: &sys::SSL_CTX, idx: c_int) -> *mut c_void; + pub fn SSL_CTX_set0_buffer_pool(ctx: &sys::SSL_CTX, pool: *mut CRYPTO_BUFFER_POOL); + pub fn SSL_CTX_set_cipher_list(ctx: &sys::SSL_CTX, str_: *const c_char) -> c_int; // ── CRYPTO_BUFFER_POOL ─────────────────────────────────────────────── pub fn CRYPTO_BUFFER_POOL_new() -> *mut CRYPTO_BUFFER_POOL; // ── SSL ────────────────────────────────────────────────────────────── - pub fn SSL_new(ctx: *mut SSL_CTX) -> *mut SSL; + pub safe fn SSL_new(ctx: &sys::SSL_CTX) -> *mut SSL; pub fn SSL_free(ssl: *mut SSL); pub fn SSL_set_connect_state(ssl: *mut SSL); pub fn SSL_set_accept_state(ssl: *mut SSL); @@ -708,11 +861,14 @@ unsafe extern "C" { pub fn SSL_get_shutdown(ssl: *const SSL) -> c_int; pub fn SSL_is_init_finished(ssl: *const SSL) -> c_int; pub fn SSL_set_verify(ssl: *mut SSL, mode: c_int, callback: SSL_verify_cb); - pub fn SSL_set0_verify_cert_store(ssl: *mut SSL, store: *mut X509_STORE) -> c_int; + // `set0`: takes ownership of `store`. Callers hand over the unit (a leaked + // `X509_STORE` handle, or a raw `+1` from `us_get_shared_default_ca_store`). + pub fn SSL_set0_verify_cert_store(ssl: *mut SSL, store: *mut sys::X509_STORE) -> c_int; pub fn SSL_set_renegotiate_mode(ssl: *mut SSL, mode: ssl_renegotiate_mode_t); pub fn SSL_renegotiate(ssl: *mut SSL) -> c_int; pub fn SSL_get_servername(ssl: *const SSL, ty: c_int) -> *const c_char; - pub fn SSL_get_SSL_CTX(ssl: *const SSL) -> *mut SSL_CTX; + // Borrowed parent context: takes no ref, so the result stays a raw pointer. + pub fn SSL_get_SSL_CTX(ssl: *const SSL) -> *mut sys::SSL_CTX; pub fn SSL_get_ex_data(ssl: *const SSL, idx: c_int) -> *mut c_void; pub fn SSL_set_ex_data(ssl: *mut SSL, idx: c_int, data: *mut c_void) -> c_int; pub fn SSL_set_tlsext_host_name(ssl: *mut SSL, name: *const c_char) -> c_int; @@ -731,7 +887,7 @@ unsafe extern "C" { supported_len: c_uint, ) -> c_int; pub fn SSL_CTX_set_alpn_select_cb( - ctx: *mut SSL_CTX, + ctx: &sys::SSL_CTX, cb: Option< unsafe extern "C" fn( ssl: *mut SSL, @@ -782,7 +938,7 @@ unsafe extern "C" { key: *const c_void, key_len: usize, md: *const EVP_MD, - impl_: *mut ENGINE, + impl_: *mut sys::ENGINE, ) -> c_int; pub fn HMAC_Update(ctx: *mut HMAC_CTX, data: *const u8, data_len: usize) -> c_int; pub fn HMAC_Final(ctx: *mut HMAC_CTX, out: *mut u8, out_len: *mut c_uint) -> c_int; diff --git a/src/bundler_jsc/analyze_jsc.rs b/src/bundler_jsc/analyze_jsc.rs index 7648b1b3763c..1828c7e786f1 100644 --- a/src/bundler_jsc/analyze_jsc.rs +++ b/src/bundler_jsc/analyze_jsc.rs @@ -16,7 +16,7 @@ use bun_bundler::analyze_transpiled_module as analyze; pub(crate) extern "C" fn zig__ModuleInfoDeserialized__toJSModuleRecord( global_object: &JSGlobalObject, vm: &VM, - module_key: &IdentifierArray, + module_key: &sys::IdentifierArray, source_code: &SourceCode, declared_variables: &mut VariableEnvironment, lexical_variables: &mut VariableEnvironment, @@ -54,13 +54,9 @@ pub(crate) extern "C" fn zig__ModuleInfoDeserialized__toJSModuleRecord( return core::ptr::null_mut(); } - let identifiers = IdentifierArray::create(strings_lens.len()); - // SAFETY: `identifiers` is non-null (returned by `create`); the scopeguard destroys it - // exactly once at scope exit (on both success and early-return paths). - let _identifiers_guard = scopeguard::guard(identifiers, |p| unsafe { - IdentifierArray::destroy(p); - }); - let identifiers: *mut IdentifierArray = *_identifiers_guard; + // Owns the array; `Drop` frees it at scope exit, on success and early returns alike. + let identifiers_owned = IdentifierArray::create(strings_lens.len()); + let identifiers: &IdentifierArray = &identifiers_owned; let mut offset: usize = 0; for (index, &len) in strings_lens.iter().enumerate() { @@ -69,8 +65,8 @@ pub(crate) extern "C" fn zig__ModuleInfoDeserialized__toJSModuleRecord( return core::ptr::null_mut(); // error! } let sub = &strings_buf[offset..offset + len]; - // SAFETY: `identifiers` is live for the scope of this fn (guard above). - unsafe { IdentifierArray::set_from_utf8(identifiers, index, vm, sub) }; + // SAFETY: `index < strings_lens.len()`, the length the array was created with. + unsafe { identifiers.set_from_utf8(index, vm, sub) }; offset += len; } @@ -221,56 +217,72 @@ unsafe extern "C" { fn JSC__VariableEnvironment__add( environment: *mut VariableEnvironment, vm: *const VM, - identifier_array: *mut IdentifierArray, + identifier_array: &sys::IdentifierArray, identifier_index: StringID, ); } impl VariableEnvironment { - // Forwards `identifier_array` to C++ without dereferencing; not_unsafe_ptr_arg_deref is a false positive on opaque-token forwarding. - #[allow(clippy::not_unsafe_ptr_arg_deref)] #[inline] - pub fn add( - &mut self, - vm: &VM, - identifier_array: *mut IdentifierArray, - identifier_index: StringID, - ) { - // SAFETY: self is a valid &mut VariableEnvironment from C++; identifier_array is live (scopeguard). - unsafe { JSC__VariableEnvironment__add(self, vm, identifier_array, identifier_index) } + pub fn add(&mut self, vm: &VM, identifier_array: &IdentifierArray, identifier_index: StringID) { + // SAFETY: `self` is a valid `&mut VariableEnvironment` handed over by C++. + unsafe { JSC__VariableEnvironment__add(self, vm, identifier_array.raw(), identifier_index) } + } +} + +/// The C++ object itself. Only the extern declarations below name this type; +/// all Rust code uses the owning [`IdentifierArray`] handle. +pub mod sys { + bun_opaque::opaque_ffi! { + /// Base of a C array of `JSC::Identifier`. `&Self` is ABI-identical to a + /// non-null `JSC::Identifier*` and carries no `noalias`/`readonly` — C++ + /// writes elements through it. + pub struct IdentifierArray; } } -bun_opaque::opaque_ffi! { pub struct IdentifierArray; } +// C++ allocates (`new Identifier[len]`) and hands back the array. One +// `IdentifierArray` handle owns that whole allocation. +bun_opaque::foreign_handle! { + /// Owned handle to a C++ `JSC::Identifier[]`. + /// + /// The pointer is the base of the array, so the handle owns every element + /// (`delete[]`), not one. Every method takes `&self`: C++ writes elements + /// through the same pointer, so there is no `&mut self` to have. + pub struct IdentifierArray(sys::IdentifierArray) via JSC__IdentifierArray__destroy; +} + unsafe extern "C" { - fn JSC__IdentifierArray__create(len: usize) -> *mut IdentifierArray; - fn JSC__IdentifierArray__destroy(identifier_array: *mut IdentifierArray); + safe fn JSC__IdentifierArray__create(len: usize) -> *mut sys::IdentifierArray; + // safe: C++ takes `Identifier*` and `delete[]`s it. Freeing is not exclusive + // access in Rust's model, so the receiver is `&`, not `&mut`. + safe fn JSC__IdentifierArray__destroy(identifier_array: &sys::IdentifierArray); + // NOT `safe fn`: C++ writes `identifierArray[n]` with no bounds check and + // reads `str_[..len]`. fn JSC__IdentifierArray__setFromUtf8( - identifier_array: *mut IdentifierArray, + identifier_array: &sys::IdentifierArray, n: usize, - vm: *const VM, + vm: &VM, str_: *const u8, len: usize, ); } + impl IdentifierArray { + /// `new Identifier[len]` on the C++ side. #[inline] - pub fn create(len: usize) -> *mut IdentifierArray { - // SAFETY: FFI call; C++ side allocates. - unsafe { JSC__IdentifierArray__create(len) } - } - /// # Safety - /// `identifier_array` must be a pointer previously returned by `create` and not yet destroyed. - #[inline] - pub unsafe fn destroy(identifier_array: *mut IdentifierArray) { - // SAFETY: caller contract — `identifier_array` came from `create` and has not been destroyed. - unsafe { JSC__IdentifierArray__destroy(identifier_array) } + pub fn create(len: usize) -> Self { + // SAFETY: `JSC__IdentifierArray__create` transfers a fresh `new Identifier[len]` + // allocation to us; no other handle frees it. + unsafe { Self::adopt_ptr(JSC__IdentifierArray__create(len)) } + .expect("JSC__IdentifierArray__create returned null") } + /// # Safety - /// `this` must be live; `n` must be in-bounds for the array's length. + /// `n` must be in-bounds for the length `self` was created with. #[inline] - pub unsafe fn set_from_utf8(this: *mut IdentifierArray, n: usize, vm: &VM, str_: &[u8]) { - // SAFETY: caller contract — `this` is live, `n` is in bounds; `str_` is a valid slice for the call. - unsafe { JSC__IdentifierArray__setFromUtf8(this, n, vm, str_.as_ptr(), str_.len()) } + pub unsafe fn set_from_utf8(&self, n: usize, vm: &VM, str_: &[u8]) { + // SAFETY: caller contract — `n` is in bounds; `str_` is valid for the call. + unsafe { JSC__IdentifierArray__setFromUtf8(self.raw(), n, vm, str_.as_ptr(), str_.len()) } } } @@ -282,7 +294,7 @@ unsafe extern "C" { fn JSC_JSModuleRecord__create( global_object: *const JSGlobalObject, vm: *const VM, - module_key: *const IdentifierArray, + module_key: &sys::IdentifierArray, source_code: *const SourceCode, declared_variables: *mut VariableEnvironment, lexical_variables: *mut VariableEnvironment, @@ -293,56 +305,56 @@ unsafe extern "C" { fn JSC_JSModuleRecord__addIndirectExport( module_record: *mut JSModuleRecord, - identifier_array: *mut IdentifierArray, + identifier_array: &sys::IdentifierArray, export_name: StringID, import_name: StringID, module_name: StringID, ); fn JSC_JSModuleRecord__addLocalExport( module_record: *mut JSModuleRecord, - identifier_array: *mut IdentifierArray, + identifier_array: &sys::IdentifierArray, export_name: StringID, local_name: StringID, ); fn JSC_JSModuleRecord__addNamespaceExport( module_record: *mut JSModuleRecord, - identifier_array: *mut IdentifierArray, + identifier_array: &sys::IdentifierArray, export_name: StringID, module_name: StringID, ); fn JSC_JSModuleRecord__addStarExport( module_record: *mut JSModuleRecord, - identifier_array: *mut IdentifierArray, + identifier_array: &sys::IdentifierArray, module_name: StringID, ); fn JSC_JSModuleRecord__addRequestedModuleNullAttributesPtr( module_record: *mut JSModuleRecord, - identifier_array: *mut IdentifierArray, + identifier_array: &sys::IdentifierArray, module_name: StringID, phase_defer: bool, ); fn JSC_JSModuleRecord__addRequestedModuleJavaScript( module_record: *mut JSModuleRecord, - identifier_array: *mut IdentifierArray, + identifier_array: &sys::IdentifierArray, module_name: StringID, phase_defer: bool, ); fn JSC_JSModuleRecord__addRequestedModuleWebAssembly( module_record: *mut JSModuleRecord, - identifier_array: *mut IdentifierArray, + identifier_array: &sys::IdentifierArray, module_name: StringID, phase_defer: bool, ); fn JSC_JSModuleRecord__addRequestedModuleJSON( module_record: *mut JSModuleRecord, - identifier_array: *mut IdentifierArray, + identifier_array: &sys::IdentifierArray, module_name: StringID, phase_defer: bool, ); fn JSC_JSModuleRecord__addRequestedModuleHostDefined( module_record: *mut JSModuleRecord, - identifier_array: *mut IdentifierArray, + identifier_array: &sys::IdentifierArray, module_name: StringID, host_defined_import_type: StringID, phase_defer: bool, @@ -350,28 +362,28 @@ unsafe extern "C" { fn JSC_JSModuleRecord__addImportEntrySingle( module_record: *mut JSModuleRecord, - identifier_array: *mut IdentifierArray, + identifier_array: &sys::IdentifierArray, import_name: StringID, local_name: StringID, module_name: StringID, ); fn JSC_JSModuleRecord__addImportEntrySingleTypeScript( module_record: *mut JSModuleRecord, - identifier_array: *mut IdentifierArray, + identifier_array: &sys::IdentifierArray, import_name: StringID, local_name: StringID, module_name: StringID, ); fn JSC_JSModuleRecord__addImportEntryNamespace( module_record: *mut JSModuleRecord, - identifier_array: *mut IdentifierArray, + identifier_array: &sys::IdentifierArray, import_name: StringID, local_name: StringID, module_name: StringID, ); fn JSC_JSModuleRecord__addImportEntryNamespaceDefer( module_record: *mut JSModuleRecord, - identifier_array: *mut IdentifierArray, + identifier_array: &sys::IdentifierArray, import_name: StringID, local_name: StringID, module_name: StringID, @@ -382,7 +394,7 @@ impl JSModuleRecord { pub(crate) fn create( global_object: &JSGlobalObject, vm: &VM, - module_key: &IdentifierArray, + module_key: &sys::IdentifierArray, source_code: &SourceCode, declared_variables: &mut VariableEnvironment, lexical_variables: &mut VariableEnvironment, @@ -412,137 +424,132 @@ impl JSModuleRecord { trait JSModuleRecordExt { fn add_indirect_export( self, - ia: *mut IdentifierArray, + ia: &IdentifierArray, export_name: StringID, import_name: StringID, module_name: StringID, ); - fn add_local_export( - self, - ia: *mut IdentifierArray, - export_name: StringID, - local_name: StringID, - ); + fn add_local_export(self, ia: &IdentifierArray, export_name: StringID, local_name: StringID); fn add_namespace_export( self, - ia: *mut IdentifierArray, + ia: &IdentifierArray, export_name: StringID, module_name: StringID, ); - fn add_star_export(self, ia: *mut IdentifierArray, module_name: StringID); + fn add_star_export(self, ia: &IdentifierArray, module_name: StringID); fn add_requested_module_null_attributes_ptr( self, - ia: *mut IdentifierArray, + ia: &IdentifierArray, module_name: StringID, phase_defer: bool, ); fn add_requested_module_java_script( self, - ia: *mut IdentifierArray, + ia: &IdentifierArray, module_name: StringID, phase_defer: bool, ); fn add_requested_module_web_assembly( self, - ia: *mut IdentifierArray, + ia: &IdentifierArray, module_name: StringID, phase_defer: bool, ); fn add_requested_module_json( self, - ia: *mut IdentifierArray, + ia: &IdentifierArray, module_name: StringID, phase_defer: bool, ); fn add_requested_module_host_defined( self, - ia: *mut IdentifierArray, + ia: &IdentifierArray, module_name: StringID, host_defined_import_type: StringID, phase_defer: bool, ); fn add_import_entry_single( self, - ia: *mut IdentifierArray, + ia: &IdentifierArray, import_name: StringID, local_name: StringID, module_name: StringID, ); fn add_import_entry_single_type_script( self, - ia: *mut IdentifierArray, + ia: &IdentifierArray, import_name: StringID, local_name: StringID, module_name: StringID, ); fn add_import_entry_namespace( self, - ia: *mut IdentifierArray, + ia: &IdentifierArray, import_name: StringID, local_name: StringID, module_name: StringID, ); fn add_import_entry_namespace_defer( self, - ia: *mut IdentifierArray, + ia: &IdentifierArray, import_name: StringID, local_name: StringID, module_name: StringID, ); } impl JSModuleRecordExt for *mut JSModuleRecord { - // SAFETY (all below): `self` is the non-null pointer returned by JSC_JSModuleRecord__create; - // `ia` is the live IdentifierArray guarded by scopeguard for the duration of the caller. + // SAFETY (all below): `self` is the non-null pointer returned by JSC_JSModuleRecord__create. #[inline] fn add_indirect_export( self, - ia: *mut IdentifierArray, + ia: &IdentifierArray, export_name: StringID, import_name: StringID, module_name: StringID, ) { - // SAFETY: `self` is the non-null record from `JSModuleRecord::create`; `ia` is kept alive by the caller's scopeguard. + // SAFETY: `self` is the non-null record from `JSModuleRecord::create`. unsafe { - JSC_JSModuleRecord__addIndirectExport(self, ia, export_name, import_name, module_name) + JSC_JSModuleRecord__addIndirectExport( + self, + ia.raw(), + export_name, + import_name, + module_name, + ) } } #[inline] - fn add_local_export( - self, - ia: *mut IdentifierArray, - export_name: StringID, - local_name: StringID, - ) { - // SAFETY: `self` is the non-null record from `JSModuleRecord::create`; `ia` is kept alive by the caller's scopeguard. - unsafe { JSC_JSModuleRecord__addLocalExport(self, ia, export_name, local_name) } + fn add_local_export(self, ia: &IdentifierArray, export_name: StringID, local_name: StringID) { + // SAFETY: `self` is the non-null record from `JSModuleRecord::create`. + unsafe { JSC_JSModuleRecord__addLocalExport(self, ia.raw(), export_name, local_name) } } #[inline] fn add_namespace_export( self, - ia: *mut IdentifierArray, + ia: &IdentifierArray, export_name: StringID, module_name: StringID, ) { - // SAFETY: `self` is the non-null record from `JSModuleRecord::create`; `ia` is kept alive by the caller's scopeguard. - unsafe { JSC_JSModuleRecord__addNamespaceExport(self, ia, export_name, module_name) } + // SAFETY: `self` is the non-null record from `JSModuleRecord::create`. + unsafe { JSC_JSModuleRecord__addNamespaceExport(self, ia.raw(), export_name, module_name) } } #[inline] - fn add_star_export(self, ia: *mut IdentifierArray, module_name: StringID) { - // SAFETY: `self` is the non-null record from `JSModuleRecord::create`; `ia` is kept alive by the caller's scopeguard. - unsafe { JSC_JSModuleRecord__addStarExport(self, ia, module_name) } + fn add_star_export(self, ia: &IdentifierArray, module_name: StringID) { + // SAFETY: `self` is the non-null record from `JSModuleRecord::create`. + unsafe { JSC_JSModuleRecord__addStarExport(self, ia.raw(), module_name) } } #[inline] fn add_requested_module_null_attributes_ptr( self, - ia: *mut IdentifierArray, + ia: &IdentifierArray, module_name: StringID, phase_defer: bool, ) { - // SAFETY: `self` is the non-null record from `JSModuleRecord::create`; `ia` is kept alive by the caller's scopeguard. + // SAFETY: `self` is the non-null record from `JSModuleRecord::create`. unsafe { JSC_JSModuleRecord__addRequestedModuleNullAttributesPtr( self, - ia, + ia.raw(), module_name, phase_defer, ) @@ -551,50 +558,62 @@ impl JSModuleRecordExt for *mut JSModuleRecord { #[inline] fn add_requested_module_java_script( self, - ia: *mut IdentifierArray, + ia: &IdentifierArray, module_name: StringID, phase_defer: bool, ) { - // SAFETY: `self` is the non-null record from `JSModuleRecord::create`; `ia` is kept alive by the caller's scopeguard. + // SAFETY: `self` is the non-null record from `JSModuleRecord::create`. unsafe { - JSC_JSModuleRecord__addRequestedModuleJavaScript(self, ia, module_name, phase_defer) + JSC_JSModuleRecord__addRequestedModuleJavaScript( + self, + ia.raw(), + module_name, + phase_defer, + ) } } #[inline] fn add_requested_module_web_assembly( self, - ia: *mut IdentifierArray, + ia: &IdentifierArray, module_name: StringID, phase_defer: bool, ) { - // SAFETY: `self` is the non-null record from `JSModuleRecord::create`; `ia` is kept alive by the caller's scopeguard. + // SAFETY: `self` is the non-null record from `JSModuleRecord::create`. unsafe { - JSC_JSModuleRecord__addRequestedModuleWebAssembly(self, ia, module_name, phase_defer) + JSC_JSModuleRecord__addRequestedModuleWebAssembly( + self, + ia.raw(), + module_name, + phase_defer, + ) } } #[inline] fn add_requested_module_json( self, - ia: *mut IdentifierArray, + ia: &IdentifierArray, module_name: StringID, phase_defer: bool, ) { - // SAFETY: `self` is the non-null record from `JSModuleRecord::create`; `ia` is kept alive by the caller's scopeguard. - unsafe { JSC_JSModuleRecord__addRequestedModuleJSON(self, ia, module_name, phase_defer) } + // SAFETY: `self` is the non-null record from `JSModuleRecord::create`. + unsafe { + JSC_JSModuleRecord__addRequestedModuleJSON(self, ia.raw(), module_name, phase_defer) + } } #[inline] fn add_requested_module_host_defined( self, - ia: *mut IdentifierArray, + ia: &IdentifierArray, module_name: StringID, host_defined_import_type: StringID, phase_defer: bool, ) { - // SAFETY: `self` is the non-null record from `JSModuleRecord::create`; `ia` is kept alive by the caller's scopeguard. + // SAFETY: `self` is the non-null record from `JSModuleRecord::create`. unsafe { JSC_JSModuleRecord__addRequestedModuleHostDefined( self, - ia, + ia.raw(), module_name, host_defined_import_type, phase_defer, @@ -604,29 +623,35 @@ impl JSModuleRecordExt for *mut JSModuleRecord { #[inline] fn add_import_entry_single( self, - ia: *mut IdentifierArray, + ia: &IdentifierArray, import_name: StringID, local_name: StringID, module_name: StringID, ) { - // SAFETY: `self` is the non-null record from `JSModuleRecord::create`; `ia` is kept alive by the caller's scopeguard. + // SAFETY: `self` is the non-null record from `JSModuleRecord::create`. unsafe { - JSC_JSModuleRecord__addImportEntrySingle(self, ia, import_name, local_name, module_name) + JSC_JSModuleRecord__addImportEntrySingle( + self, + ia.raw(), + import_name, + local_name, + module_name, + ) } } #[inline] fn add_import_entry_single_type_script( self, - ia: *mut IdentifierArray, + ia: &IdentifierArray, import_name: StringID, local_name: StringID, module_name: StringID, ) { - // SAFETY: `self` is the non-null record from `JSModuleRecord::create`; `ia` is kept alive by the caller's scopeguard. + // SAFETY: `self` is the non-null record from `JSModuleRecord::create`. unsafe { JSC_JSModuleRecord__addImportEntrySingleTypeScript( self, - ia, + ia.raw(), import_name, local_name, module_name, @@ -636,16 +661,16 @@ impl JSModuleRecordExt for *mut JSModuleRecord { #[inline] fn add_import_entry_namespace( self, - ia: *mut IdentifierArray, + ia: &IdentifierArray, import_name: StringID, local_name: StringID, module_name: StringID, ) { - // SAFETY: `self` is the non-null record from `JSModuleRecord::create`; `ia` is kept alive by the caller's scopeguard. + // SAFETY: `self` is the non-null record from `JSModuleRecord::create`. unsafe { JSC_JSModuleRecord__addImportEntryNamespace( self, - ia, + ia.raw(), import_name, local_name, module_name, @@ -655,16 +680,16 @@ impl JSModuleRecordExt for *mut JSModuleRecord { #[inline] fn add_import_entry_namespace_defer( self, - ia: *mut IdentifierArray, + ia: &IdentifierArray, import_name: StringID, local_name: StringID, module_name: StringID, ) { - // SAFETY: `self` is the non-null record from `JSModuleRecord::create`; `ia` is kept alive by the caller's scopeguard. + // SAFETY: `self` is the non-null record from `JSModuleRecord::create`. unsafe { JSC_JSModuleRecord__addImportEntryNamespaceDefer( self, - ia, + ia.raw(), import_name, local_name, module_name, diff --git a/src/cares_sys/c_ares.rs b/src/cares_sys/c_ares.rs index efdc8ddd3158..390c1c8800d3 100644 --- a/src/cares_sys/c_ares.rs +++ b/src/cares_sys/c_ares.rs @@ -698,10 +698,9 @@ bun_opaque::opaque_ffi! { /// mutates the channel on every dispatch/process call). pub struct Channel; } -// Load-bearing: `ares_cancel`/`ares_process_fd` are declared `safe fn(&mut Channel)` -// on the basis that re-entrant callbacks re-deriving `&mut Channel` from a raw -// pointer cannot conflict because `Channel` claims zero bytes. If this type ever -// gains a non-ZST field, those signatures must revert to `unsafe fn(*mut Channel)`. +// `Channel` is `!Freeze`, so `&Channel` asserts neither `noalias` nor `readonly`; +// c-ares owns the real object and re-enters through the same handle. If this type +// ever gains a non-ZST field, the `safe fn(&Channel)` externs must be revisited. const _: () = assert!(core::mem::size_of::() == 0); /// Implemented by the type that owns a `*mut Channel` and receives socket- @@ -805,7 +804,7 @@ impl Channel { /// See c-ares `ares_getaddrinfo` documentation. pub fn get_addr_info( - &mut self, + &self, host: &[u8], port: u16, hints: &[AddrInfo_hints], @@ -833,7 +832,7 @@ impl Channel { // SAFETY: c-ares FFI; host/port/hints are NUL-terminated stack buffers or null; ctx outlives the channel. unsafe { ares_getaddrinfo( - self, + self.as_mut_ptr(), host_ptr, port_ptr, hints_, @@ -843,7 +842,7 @@ impl Channel { } } - pub fn resolve(&mut self, name: &[u8], ctx: &mut T) { + pub fn resolve(&self, name: &[u8], ctx: &mut T) { if name.len() >= 1023 || name.contains(&0) || (name.is_empty() && !(T::LOOKUP_NAME == b"ns" || T::LOOKUP_NAME == b"soa")) @@ -867,7 +866,7 @@ impl Channel { // SAFETY: c-ares FFI; name_ptr is a NUL-terminated stack buffer; ctx outlives the channel. unsafe { ares_query( - self, + self.as_mut_ptr(), name_ptr, NSClass::ns_c_in, T::NS_TYPE, @@ -877,7 +876,7 @@ impl Channel { } } - pub fn get_host_by_addr(&mut self, ip_addr: &[u8], ctx: &mut T) { + pub fn get_host_by_addr(&self, ip_addr: &[u8], ctx: &mut T) { // "0000:0000:0000:0000:0000:ffff:192.168.100.228".length = 45 const BUF_SIZE: usize = 46; let mut addr_buf = [0u8; BUF_SIZE]; @@ -900,7 +899,7 @@ impl Channel { // SAFETY: c-ares FFI; addr holds a 4-byte in_addr written by ares_inet_pton; ctx outlives the channel. unsafe { ares_gethostbyaddr( - self, + self.as_mut_ptr(), addr.as_ptr().cast::(), 4, AF::INET, @@ -917,7 +916,7 @@ impl Channel { // SAFETY: c-ares FFI; addr holds a 16-byte in6_addr written by ares_inet_pton; ctx outlives the channel. unsafe { ares_gethostbyaddr( - self, + self.as_mut_ptr(), addr.as_ptr().cast::(), 16, AF::INET6, @@ -940,7 +939,7 @@ impl Channel { } /// https://c-ares.org/ares_getnameinfo.html - pub fn get_name_info(&mut self, sa: &mut sockaddr, ctx: &mut T) { + pub fn get_name_info(&self, sa: &mut sockaddr, ctx: &mut T) { let salen: ares_socklen_t = if sa.sa_family == AF::INET as _ { core::mem::size_of::() as ares_socklen_t } else { @@ -949,7 +948,7 @@ impl Channel { // SAFETY: c-ares FFI; sa is a valid sockaddr of size `salen`; ctx outlives the channel. unsafe { ares_getnameinfo( - self, + self.as_mut_ptr(), std::ptr::from_ref::(sa), salen, // node returns ENOTFOUND for addresses like 255.255.255.255:80 @@ -962,7 +961,7 @@ impl Channel { } #[inline] - pub fn process(&mut self, fd: ares_socket_t, readable: bool, writable: bool) { + pub fn process(&self, fd: ares_socket_t, readable: bool, writable: bool) { ares_process_fd( self, if readable { fd } else { ARES_SOCKET_BAD }, @@ -1027,15 +1026,11 @@ unsafe extern "C" { pub fn ares_destroy_options(options: *mut Options); pub fn ares_dup(dest: *mut Channel, src: *mut Channel) -> c_int; pub fn ares_destroy(channel: *mut Channel); - // Opaque handle by exclusive reference only — `Channel` is `!Freeze`/`!Sync` - // (UnsafeCell + PhantomData<*mut u8>). Note: `ares_cancel`/`ares_process_fd` - // synchronously invoke stored completion callbacks which may re-enter the - // resolver and re-derive a `&mut Channel` from a raw pointer; this is sound - // because `Channel` is a ZST (`UnsafeCell<[u8;0]>`), so `&mut Channel` - // claims zero bytes and overlapping `&mut` do not conflict under Stacked - // Borrows — the borrow checker does NOT gate the raw-pointer callbacks. - pub safe fn ares_cancel(channel: &mut Channel); - pub safe fn ares_set_local_ip4(channel: &mut Channel, local_ip: c_uint); + // Shared handle: `Channel` is `!Freeze`, so `&Channel` carries no `noalias` + // and no `readonly`. `ares_cancel`/`ares_process_fd` synchronously invoke + // completion callbacks that re-enter through the same pointer. + pub safe fn ares_cancel(channel: &Channel); + pub safe fn ares_set_local_ip4(channel: &Channel, local_ip: c_uint); pub fn ares_set_local_ip6(channel: *mut Channel, local_ip6: *const u8); pub fn ares_set_local_dev(channel: *mut Channel, local_dev_name: *const u8); pub fn ares_set_socket_callback( @@ -1151,11 +1146,7 @@ unsafe extern "C" { ) -> *mut struct_timeval; // pub fn ares_process(channel: *mut Channel, read_fds: *mut fd_set, write_fds: *mut fd_set); // Opaque handle by exclusive reference + scalars only. - pub safe fn ares_process_fd( - channel: &mut Channel, - read_fd: ares_socket_t, - write_fd: ares_socket_t, - ); + pub safe fn ares_process_fd(channel: &Channel, read_fd: ares_socket_t, write_fd: ares_socket_t); pub fn ares_create_query( name: *const c_char, dnsclass: c_int, diff --git a/src/codegen/cppbind.ts b/src/codegen/cppbind.ts index b3f10957ae40..383bd5ea6644 100644 --- a/src/codegen/cppbind.ts +++ b/src/codegen/cppbind.ts @@ -461,7 +461,8 @@ const rustSharedTypes: Record = { "JSC::JSPromise": "crate::JSPromise", "JSC::JSMap": "crate::JSMap", "JSC::CustomGetterSetter": "crate::CustomGetterSetter", - "JSC::SourceProvider": "crate::SourceProvider", + // `crate::SourceProvider` is the owning handle; the C++ object is the ZST. + "JSC::SourceProvider": "crate::source_provider::sys::SourceProvider", "JSC::CallFrame": "crate::CallFrame", "JSC::JSObject": "crate::JSObject", "JSC::JSString": "crate::JSString", @@ -469,7 +470,7 @@ const rustSharedTypes: Record = { "JSC::JSInternalPromise": "crate::JSInternalPromise", "WTF::StringImpl": "core::ffi::c_void", "WebCore::DOMURL": "crate::DOMURL", - "WebCore::EventLoopTask": "crate::cpp_task::CppTask", + "WebCore::EventLoopTask": "crate::cpp_task::sys::CppTask", // HTTPServerAgent / inspector types only show up in `nothrow` exports; // emit as opaque so the raw extern still type-checks. "Inspector::InspectorHTTPServerAgent": "core::ffi::c_void", diff --git a/src/http/HTTPContext.rs b/src/http/HTTPContext.rs index d5db29f77305..a45db34d0894 100644 --- a/src/http/HTTPContext.rs +++ b/src/http/HTTPContext.rs @@ -8,7 +8,7 @@ use crate::{ self as http, AlpnOffer, HTTPCertError, HTTPClient, InitError, get_cert_error_from_no, h2, }; use bun_boringssl::ssl_ctx_setup; -use bun_boringssl_sys::SSL_CTX; +use bun_boringssl_sys::sys::SSL_CTX; use bun_collections::{HiveArray, TaggedPtrUnion}; use bun_core::strings; use bun_core::{self, Error, FeatureFlags}; @@ -1108,8 +1108,7 @@ impl Drop for HTTPContext { } if SSL { if let Some(c) = self.secure { - // SAFETY: we own one ref on the SSL_CTX. - unsafe { bun_boringssl_sys::SSL_CTX_free(c) }; + bun_boringssl_sys::SSL_CTX_free(SSL_CTX::opaque_ref(c)); } } // Note: `bun.default_allocator.destroy(this)` is the Box drop diff --git a/src/http/HTTPThread.rs b/src/http/HTTPThread.rs index d0a67b09efbf..b1d1e53fbb18 100644 --- a/src/http/HTTPThread.rs +++ b/src/http/HTTPThread.rs @@ -276,25 +276,26 @@ pub struct CertCheckResumeMessage { } pub struct LibdeflateState { - pub decompressor: Option, - pub compressor: Option, + pub decompressor: Option, + pub compressor: Option, pub shared_buffer: [u8; 512 * 1024], } -// SAFETY: `Option` is `#[repr(transparent)]` over -// `NonNull`, so all-zero = `None`; `[u8; N]` is valid at the all-zero bit -// pattern. +// SAFETY: `Option` / `Option` are +// `#[repr(transparent)]` over `NonNull`, so all-zero = `None`; `[u8; N]` is +// valid at the all-zero bit pattern. unsafe impl bun_core::Zeroable for LibdeflateState {} impl LibdeflateState { - /// Mutable access to the libdeflate decompressor handle. + /// Access to the libdeflate decompressor handle. `&` suffices: libdeflate + /// mutates the decompressor's scratch state through a shared borrow. /// /// `decompressor` is set once in [`HttpThread::deflater`] (panics on OOM) /// and is never `None` after that, so the unwrap is infallible. #[inline] - pub(crate) fn decompressor_mut(&mut self) -> &mut bun_libdeflate_sys::libdeflate::Decompressor { + pub(crate) fn decompressor(&self) -> &bun_libdeflate_sys::libdeflate::Decompressor { self.decompressor - .as_deref_mut() + .as_ref() .expect("set in HttpThread::deflater()") } } @@ -419,7 +420,7 @@ impl HttpThread { pub fn deflater(&mut self) -> &mut LibdeflateState { if self.lazy_libdeflater.is_none() { - let decompressor = bun_libdeflate_sys::libdeflate::OwnedDecompressor::new() + let decompressor = bun_libdeflate_sys::libdeflate::Decompressor::new() .unwrap_or_else(|| bun_core::out_of_memory()); let mut state: Box = bun_core::boxed_zeroed(); state.decompressor = Some(decompressor); diff --git a/src/http/InternalState.rs b/src/http/InternalState.rs index 307eebc38ee3..c6e39db8ba6c 100644 --- a/src/http/InternalState.rs +++ b/src/http/InternalState.rs @@ -313,7 +313,7 @@ impl<'a> InternalState<'a> { (estimated_size as usize).saturating_sub(body_out_str.list.len()), ); body_out_str.list.clear(); - let result = deflater.decompressor_mut().decompress_to_vec( + let result = deflater.decompressor().decompress_to_vec( buffer, &mut body_out_str.list, bun_libdeflate::Encoding::Gzip, @@ -326,9 +326,11 @@ impl<'a> InternalState<'a> { } } + // Field access, not `deflater.decompressor()`: keeps the borrow + // field-split so `&mut deflater.shared_buffer` below still holds. let decompressor = deflater .decompressor - .as_deref_mut() + .as_ref() .expect("set in HttpThread::deflater()"); let result = decompressor.decompress( buffer, diff --git a/src/http/compress_body.rs b/src/http/compress_body.rs index 851047752a6b..25393325cb1f 100644 --- a/src/http/compress_body.rs +++ b/src/http/compress_body.rs @@ -92,7 +92,7 @@ fn compress_libdeflate_fast( enc: bun_libdeflate_sys::libdeflate::Encoding, level: Option, ) -> Option { - use bun_libdeflate_sys::libdeflate::{Compressor, OwnedCompressor}; + use bun_libdeflate_sys::libdeflate::Compressor; // Split-borrow so the compressor handle and `shared_buffer` can be used // together. @@ -102,7 +102,7 @@ fn compress_libdeflate_fast( .. } = state; let cached = compressor.get_or_insert_with(|| { - OwnedCompressor::new(DEFAULT_DEFLATE_LEVEL).unwrap_or_else(|| bun_core::out_of_memory()) + Compressor::new(DEFAULT_DEFLATE_LEVEL).unwrap_or_else(|| bun_core::out_of_memory()) }); // Bound is level-independent — use the cached handle so the slow-path @@ -113,10 +113,10 @@ fn compress_libdeflate_fast( // Custom level → allocate a temporary compressor; the cached handle is // pinned to DEFAULT_DEFLATE_LEVEL. - let mut tmp: Option = None; - let compressor: &mut Compressor = match level { + let mut tmp: Option = None; + let compressor: &Compressor = match level { Some(l) if l != DEFAULT_DEFLATE_LEVEL => { - tmp.insert(OwnedCompressor::new(l).unwrap_or_else(|| bun_core::out_of_memory())) + tmp.insert(Compressor::new(l).unwrap_or_else(|| bun_core::out_of_memory())) } _ => cached, }; diff --git a/src/http/h3_client/ClientContext.rs b/src/http/h3_client/ClientContext.rs index d4c7ee94096a..9826473c4c1d 100644 --- a/src/http/h3_client/ClientContext.rs +++ b/src/http/h3_client/ClientContext.rs @@ -40,15 +40,15 @@ static INSTANCE: bun_core::AtomicCell>> = static LSQUIC_INIT_ONCE: std::sync::Once = std::sync::Once::new(); impl ClientContext { - /// Mutable access to the lsquic client engine. + /// Access to the lsquic client engine. /// /// INVARIANT: `qctx` is set once in `get_or_create` to a fresh /// `us_quic_socket_context_t` and is never freed (process-lifetime, same as - /// this singleton). HTTP-thread only, so the `&mut` is the sole live borrow. + /// this singleton). #[inline] - fn qctx_mut(&mut self) -> &mut quic::Context { + fn qctx(&self) -> &quic::Context { // SAFETY: see INVARIANT above. - unsafe { &mut *self.qctx.as_ptr() } + unsafe { self.qctx.as_ref() } } /// Non-null pointer to the leaked process-lifetime singleton, if created. @@ -94,11 +94,11 @@ impl ClientContext { qctx, sessions: Vec::new(), }); - // Route through the existing [`qctx_mut`] / [`as_mut`] accessors (one - // centralised unsafe each) instead of an open-coded `qctx.as_mut()`. + // Route through the existing [`qctx`] / [`as_mut`] accessors (one + // centralised unsafe each) instead of an open-coded `qctx.as_ref()`. // `self_` is the freshly-boxed sole owner; callbacks don't fire until // the loop runs, so registering after construction is order-neutral. - callbacks::register(Self::as_mut(self_).qctx_mut()); + callbacks::register(Self::as_mut(self_).qctx()); INSTANCE.store(Some(self_)); Some(self_) } @@ -139,9 +139,9 @@ impl ClientContext { self.sessions.push(session); session_mut(session).enqueue(client); - let result = - self.qctx_mut() - .connect(host_z, port, host_z, reject, session.cast::()); + let result = self + .qctx() + .connect(host_z, port, host_z, reject, session.cast::()); match result { ConnectResult::Socket(qs) => { session_mut(session).qsocket = NonNull::new(qs); @@ -164,7 +164,7 @@ impl ClientContext { bstr::BStr::new(hostname), port, ); - let l = self.qctx_mut().r#loop(); + let l = self.qctx().r#loop(); PendingConnect::register(session, pending, l.cast::()); } ConnectResult::Err => { diff --git a/src/http/h3_client/ClientSession.rs b/src/http/h3_client/ClientSession.rs index 6543bf179094..6a4d394ffb4e 100644 --- a/src/http/h3_client/ClientSession.rs +++ b/src/http/h3_client/ClientSession.rs @@ -132,10 +132,10 @@ impl ClientSession { if let crate::HTTPRequestBody::Stream(s) = &mut client.state.original_request_body { s.ended = ended; } - // `drain_send_body` needs `&mut Stream` and `&mut quic::Stream` at - // once; they are disjoint objects, so bypass `Stream::qstream_mut`. + // `drain_send_body` needs `&mut Stream` and `&quic::Stream` at once; + // they are disjoint objects, so bypass `Stream::qstream_ref`. if let Some(qs) = stream.qstream { - encode::drain_send_body(stream, quic_stream_mut(qs.as_ptr())); + encode::drain_send_body(stream, quic_stream_ref(qs.as_ptr())); } return; } @@ -151,7 +151,7 @@ impl ClientSession { continue; } if core::mem::take(&mut stream.read_paused) { - if let Some(qs) = stream.qstream_mut() { + if let Some(qs) = stream.qstream_ref() { qs.want_read(true); } } @@ -167,7 +167,7 @@ impl ClientSession { } st.client = None; let request_body_done = st.request_body_done; - if let Some(qs) = st.qstream_mut() { + if let Some(qs) = st.qstream_ref() { qs.ext::().set(None); // The success path can reach here while the request body is still // being written (server responded early). FIN would be a @@ -412,16 +412,18 @@ pub(super) fn quic_socket_mut<'a>(qs: *mut quic::Socket) -> &'a mut quic::Socket unsafe { &mut *qs } } -/// Upgrade a non-null `*mut quic::Stream` lsquic FFI handle to `&mut`. +/// Upgrade a non-null `*mut quic::Stream` lsquic FFI handle to `&`. /// /// Same INVARIANT as [`quic_socket_mut`] — lsquic-owned, live for the /// borrow's duration (callback argument, or `Stream.qstream` set in /// `on_stream_open` and nulled in `on_stream_close` / `detach`), FFI -/// allocation distinct from any Rust holder, HTTP-thread-only. +/// allocation distinct from any Rust holder, HTTP-thread-only. `quic::Stream` +/// is an opaque FFI ZST whose methods all take `&self`, so there is no `&mut` +/// to hand out — lsquic mutates the real stream through the handle. #[inline(always)] -pub(super) fn quic_stream_mut<'a>(s: *mut quic::Stream) -> &'a mut quic::Stream { +pub(super) fn quic_stream_ref<'a>(s: *mut quic::Stream) -> &'a quic::Stream { // SAFETY: see [`quic_socket_mut`] INVARIANT. - unsafe { &mut *s } + unsafe { &*s } } /// Upgrade a `*mut Stream` (a `self.pending` entry, or one just removed from diff --git a/src/http/h3_client/PendingConnect.rs b/src/http/h3_client/PendingConnect.rs index 6c407192c3b7..d66ec3533a2e 100644 --- a/src/http/h3_client/PendingConnect.rs +++ b/src/http/h3_client/PendingConnect.rs @@ -38,7 +38,7 @@ impl Drop for PendingConnect { } impl PendingConnect { - /// Mutable access to the owned `quic::PendingConnect` C handle. + /// Shared access to the owned `quic::PendingConnect` C handle. /// /// INVARIANT: `pc` is set once in [`register`] to a live /// `us_quic_pending_connect_t` and is consumed by exactly one of @@ -47,9 +47,9 @@ impl PendingConnect { /// every caller. Centralises the raw `(*this.pc)` upgrade repeated at /// each consume site. #[inline] - fn pc_mut(&mut self) -> &mut quic::PendingConnect { + fn pc(&self) -> &quic::PendingConnect { // SAFETY: see INVARIANT above. - unsafe { &mut *self.pc } + unsafe { &*self.pc } } pub fn register(session: *mut ClientSession, pc: *mut quic::PendingConnect, l: *mut uws::Loop) { @@ -57,14 +57,14 @@ impl PendingConnect { // holds one ref from construction until Drop. `session_mut` centralises // the backref upgrade (same invariant as the other call sites below). session_mut(session).ref_(); - let mut self_ = Box::new(PendingConnect { + let self_ = Box::new(PendingConnect { session, pc, loop_ptr: l, }); - // Route the addrinfo read through the existing [`pc_mut`] accessor + // Route the addrinfo read through the existing [`pc`] accessor // (centralised raw upgrade) instead of an open-coded `(*pc)` deref. - let addrinfo = self_.pc_mut().addrinfo(); + let addrinfo = self_.pc().addrinfo(); let self_ = bun_core::heap::into_raw(self_); // SAFETY: `self_` is the Box we just leaked above and is consumed by // `on_dns_resolved` (via the global cache's notify path). @@ -80,7 +80,7 @@ impl PendingConnect { pub unsafe fn on_dns_resolved(this: *mut PendingConnect) { // SAFETY: `this` was heap-allocated in `register`; reclaim it so the Box drops at // end of scope — `Drop` derefs `session` and the allocation is freed. - let mut this = unsafe { bun_core::heap::take(this) }; + let this = unsafe { bun_core::heap::take(this) }; let session = this.session; // session is kept alive by the ref `this` holds for the duration of this @@ -88,23 +88,24 @@ impl PendingConnect { let s = session_mut(session); if s.closed || s.pending.is_empty() { // Every waiter was aborted while DNS was in flight; don't open a - // connection nobody will use. `pc_mut` upgrades the owned C + // connection nobody will use. `pc` upgrades the owned C // handle; `cancel()` consumes it. - this.pc_mut().cancel(); + this.pc().cancel(); if !s.closed { Self::fail_session(session, bun_core::err!("Aborted")); } return; } - // `pc_mut` upgrades the owned C handle; `resolved()` consumes it and + // `pc` upgrades the owned C handle; `resolved()` consumes it and // returns the connected quic socket or None on DNS failure. - let Some(qs) = this.pc_mut().resolved() else { + let Some(qs) = this.pc().resolved() else { Self::fail_session(session, bun_core::err!("DNSResolutionFailed")); return; }; - s.qsocket = Some(NonNull::from(&mut *qs)); - // qs.ext() returns the per-socket user storage slot for ClientSession. - *qs.ext::() = NonNull::new(session); + s.qsocket = Some(qs); + // `ext()` hands back the per-socket user storage slot, so it needs the + // exclusive borrow; `opaque_mut` is the centralised non-null deref. + *quic::Socket::opaque_mut(qs.as_ptr()).ext::() = NonNull::new(session); } /// DNS worker may call from off the HTTP thread; mirror diff --git a/src/http/h3_client/Stream.rs b/src/http/h3_client/Stream.rs index db8768297f42..47abe3b050ad 100644 --- a/src/http/h3_client/Stream.rs +++ b/src/http/h3_client/Stream.rs @@ -59,16 +59,16 @@ impl Stream { })) } - /// Mutable access to the bound lsquic stream handle. + /// Access to the bound lsquic stream handle. /// /// INVARIANT: `qstream` is set in `callbacks::on_stream_open` and remains /// valid until `callbacks::on_stream_close` / `ClientSession::detach` - /// nulls it. HTTP-thread-only. Borrows `self` exclusively so no two live - /// `&mut quic::Stream` can be minted from one `Stream`. + /// nulls it. HTTP-thread-only. `quic::Stream` is an opaque FFI ZST, so `&` + /// carries no `noalias` — lsquic mutates the stream through the handle. #[inline] - pub fn qstream_mut(&mut self) -> Option<&mut quic::Stream> { + pub fn qstream_ref(&self) -> Option<&quic::Stream> { self.qstream - .map(|qs| super::client_session::quic_stream_mut(qs.as_ptr())) + .map(|qs| super::client_session::quic_stream_ref(qs.as_ptr())) } /// Mutable access to the owning `ClientSession`. @@ -87,7 +87,7 @@ impl Stream { } pub fn abort(&mut self) { - if let Some(qs) = self.qstream_mut() { + if let Some(qs) = self.qstream_ref() { qs.close(); } } diff --git a/src/http/h3_client/callbacks.rs b/src/http/h3_client/callbacks.rs index 6574bf7e902d..d2c9447bbdf0 100644 --- a/src/http/h3_client/callbacks.rs +++ b/src/http/h3_client/callbacks.rs @@ -35,13 +35,13 @@ fn qsocket_arg<'a>(qs: *mut quic::Socket) -> &'a mut quic::Socket { super::client_session::quic_socket_mut(qs) } -/// Upgrade an lsquic-supplied `*mut quic::Stream` callback argument to `&mut`. +/// Upgrade an lsquic-supplied `*mut quic::Stream` callback argument to `&`. /// Same INVARIANT as [`qsocket_arg`] (lsquic-owned, live for the callback, /// HTTP-thread-only). Routes through the shared -/// [`client_session::quic_stream_mut`] accessor. +/// [`client_session::quic_stream_ref`] accessor. #[inline(always)] -fn qstream_arg<'a>(s: *mut quic::Stream) -> &'a mut quic::Stream { - super::client_session::quic_stream_mut(s) +fn qstream_arg<'a>(s: *mut quic::Stream) -> &'a quic::Stream { + super::client_session::quic_stream_ref(s) } /// Recover the `ClientSession` from a `quic::Socket`'s ext slot. @@ -65,16 +65,15 @@ fn session_of<'a>(qs: &mut quic::Socket) -> Option<&'a mut ClientSession> { /// INVARIANT: the slot is set in `on_stream_open` (and cleared in `detach`); /// the `Stream` is heap-owned by its `ClientSession` (`pending` list) and lives /// until `detach()`. HTTP-thread only, and a distinct allocation from the -/// `quic::Stream`, so the returned `&mut` neither aliases the caller's -/// `&mut quic::Stream` nor any other live borrow. +/// `quic::Stream`, so the returned `&mut` does not alias any other live borrow. #[inline] -fn stream_of<'a>(s: &mut quic::Stream) -> Option<&'a mut Stream> { +fn stream_of<'a>(s: &quic::Stream) -> Option<&'a mut Stream> { // Route through `client_session::stream_mut` (one centralised unsafe); // the ext slot is `Option>` — same backref invariant. s.ext::().get().map(|p| stream_mut(p.as_ptr())) } -pub(crate) fn register(qctx: &mut quic::Context) { +pub(crate) fn register(qctx: &quic::Context) { qctx.on_hsk_done(on_hsk_done); qctx.on_goaway(on_goaway); qctx.on_close(on_conn_close); @@ -194,7 +193,7 @@ extern "C" fn on_stream_open(s: *mut quic::Stream, is_client: c_int) { }; // `stream` is a live element of `session.pending` — `stream_mut` // centralises that upgrade invariant. - stream_mut(stream).qstream = Some(NonNull::from(&mut *s)); + stream_mut(stream).qstream = Some(NonNull::from(s)); s.ext::().set(NonNull::new(stream)); bun_core::scoped_log!(h3_client, "stream_open"); if let Err(e) = encode::write_request(session, stream_mut(stream), s) { diff --git a/src/http/h3_client/encode.rs b/src/http/h3_client/encode.rs index 871442aea6e0..a05a99a62223 100644 --- a/src/http/h3_client/encode.rs +++ b/src/http/h3_client/encode.rs @@ -20,7 +20,7 @@ use crate::{HTTPClient, HTTPVerboseLevel, Protocol}; pub fn write_request( session: &ClientSession, stream: &mut Stream, - qs: &mut quic::Stream, + qs: &quic::Stream, ) -> Result<(), bun_core::Error> { let Some(client_ptr) = stream.client else { return Err(err!(Aborted)); @@ -147,7 +147,7 @@ pub fn write_request( /// Push as much of the request body onto `qs` as flow control allows. Called /// from `write_request`, `callbacks.on_stream_writable`, and /// `ClientSession.stream_body_by_http_id` (when the JS sink delivers more bytes). -pub(crate) fn drain_send_body(stream: &mut Stream, qs: &mut quic::Stream) { +pub(crate) fn drain_send_body(stream: &mut Stream, qs: &quic::Stream) { if stream.request_body_done { return; } diff --git a/src/http_jsc/websocket_client.rs b/src/http_jsc/websocket_client.rs index 76b6c50fd05d..8ab5b90ce04e 100644 --- a/src/http_jsc/websocket_client.rs +++ b/src/http_jsc/websocket_client.rs @@ -173,8 +173,8 @@ impl WebSocket { self.message_is_compressed.set(false); self.deflate.replace(None); if let Some(s) = self.secure.take() { - // SAFETY: s is a valid SSL_CTX* owned by us per field invariant - unsafe { boringssl::c::SSL_CTX_free(s) }; + // `s` is an owned SSL_CTX ref per the field invariant. + boringssl::c::SSL_CTX_free(SslCtx::opaque_ref(s)); } // Detach the tunnel first so its shutdown callbacks cannot re-enter this path. if let Some(tunnel) = self.proxy_tunnel.take() { diff --git a/src/http_jsc/websocket_client/WebSocketDeflate.rs b/src/http_jsc/websocket_client/WebSocketDeflate.rs index 6fa1ce572da2..459c6e4e1505 100644 --- a/src/http_jsc/websocket_client/WebSocketDeflate.rs +++ b/src/http_jsc/websocket_client/WebSocketDeflate.rs @@ -33,7 +33,7 @@ impl Params { #[derive(Default)] pub struct RareData { - libdeflate_decompressor: Option, + libdeflate_decompressor: Option, // PERF: a 128KB inline buffer reused as scratch for (de)compression // output could avoid per-call allocation — profile if hot. } @@ -46,11 +46,11 @@ impl RareData { Vec::with_capacity(Self::STACK_BUFFER_SIZE) } - pub fn decompressor(&mut self) -> Option<&mut libdeflate_sys::Decompressor> { + pub fn decompressor(&mut self) -> Option<&libdeflate_sys::Decompressor> { if self.libdeflate_decompressor.is_none() { - self.libdeflate_decompressor = libdeflate_sys::OwnedDecompressor::new(); + self.libdeflate_decompressor = libdeflate_sys::Decompressor::new(); } - self.libdeflate_decompressor.as_deref_mut() + self.libdeflate_decompressor.as_ref() } } diff --git a/src/http_jsc/websocket_client/WebSocketUpgradeClient.rs b/src/http_jsc/websocket_client/WebSocketUpgradeClient.rs index b6cb5c898cea..4e315a48a8ea 100644 --- a/src/http_jsc/websocket_client/WebSocketUpgradeClient.rs +++ b/src/http_jsc/websocket_client/WebSocketUpgradeClient.rs @@ -35,7 +35,7 @@ use bun_io::KeepAlive; use bun_jsc::{JSGlobalObject, JsCell, VirtualMachineRef}; use bun_picohttp as picohttp; use bun_ptr::ThisPtr; -use bun_uws::{self as uws, SocketHandler, SocketKind, SslCtx}; +use bun_uws::{self as uws, SocketHandler, SocketKind}; use super::cpp_websocket::CppWebSocket; use super::websocket_deflate as WebSocketDeflate; @@ -87,27 +87,6 @@ enum State { Done, } -/// Owned +1 reference to a `us_ssl_ctx_t` (`SSL_CTX*`); releases the ref via -/// `SSL_CTX_free` on drop (BoringSSL decrements its internal refcount). -/// Either dropped here, or transferred to the connected `WebSocket` via -/// `into_raw()` after the upgrade completes. -struct SslCtxOwned(*mut SslCtx); - -impl SslCtxOwned { - /// Transfer ownership of the retained ref to the caller without freeing. - fn into_raw(self) -> *mut SslCtx { - core::mem::ManuallyDrop::new(self).0 - } -} - -impl Drop for SslCtxOwned { - fn drop(&mut self) { - // SAFETY: `self.0` is an owned retained ref (returned with +1 by - // `ssl_ctx_cache_get_or_create`) that has not been transferred out. - unsafe { boringssl::c::SSL_CTX_free(self.0) }; - } -} - /// WebSocket HTTP upgrade client, generic over `SSL`. /// /// Intrusive single-thread @@ -141,7 +120,7 @@ pub struct HTTPClient { /// Heap-allocated because ownership transfers to the connected /// `WebSocket` after the upgrade completes (so the `SSL_CTX` outlives /// this struct). RAII: dropping the wrapper releases the retained ref. - secure: Option, + secure: Option, /// Expected Sec-WebSocket-Accept value for handshake validation per RFC 6455 §4.2.2. /// This is base64(SHA-1(Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")). @@ -443,7 +422,8 @@ impl HTTPClient { }; // Owned ref; transferred to the connected WebSocket on // upgrade, freed in `deinit` if we never get that far. - client_box.secure = Some(SslCtxOwned(ctx)); + // SAFETY: `ssl_ctx_cache_get_or_create` handed back a +1. + client_box.secure = unsafe { boringssl::c::SSL_CTX::adopt_ptr(ctx) }; break 'brk Some(ctx); } } @@ -590,7 +570,7 @@ impl HTTPClient { } // ssl_config: Option> — Drop runs SSLConfig::deinit + frees the box. self.ssl_config = None; - // secure: Option — Drop releases the ref taken in `connect`. + // secure: Option — Drop releases the ref taken in `connect`. self.secure = None; } @@ -1698,9 +1678,9 @@ impl HTTPClient { } else { None }, - // ownership transferred; `into_raw` suppresses the - // RAII release at fn end. - saved_secure.take().map(|s| &mut *s.into_raw()), + // ownership transferred; `leak` suppresses the RAII + // release at fn end. + saved_secure.take().map(|s| &mut *s.leak().as_ptr()), ) }; } else { diff --git a/src/install/TarballStream.rs b/src/install/TarballStream.rs index c440e0f76f8d..a266ef5f9755 100644 --- a/src/install/TarballStream.rs +++ b/src/install/TarballStream.rs @@ -103,7 +103,7 @@ pub struct TarballStream { reading: Vec, read_pos: usize, - archive: Option<*mut lib::Archive>, + archive: Option<*mut lib::sys::Archive>, /// Where we are in the per-entry state machine between drain /// invocations. libarchive preserves everything else (filter buffers, @@ -585,7 +585,7 @@ impl TarballStream { /// lifetime of the archive — see `step()` # Safety for the provenance /// requirement. unsafe fn open_archive(this: *mut Self) -> Result<(), bun_core::Error> { - let archive = lib::Archive::read_new(); + let archive = lib::sys::Archive::read_new(); let guard = scopeguard::guard(archive, |a| { // SAFETY: errdefer cleanup — archive is a valid handle from read_new(). unsafe { @@ -593,19 +593,18 @@ impl TarballStream { let _ = (*a).read_free(); } }); + // SAFETY: `read_new()` asserts non-null; the handle outlives `a`. + let a: &lib::sys::Archive = unsafe { &*archive }; // Bypass bidding entirely: the stream is always gzip → tar, and // bidding would try to read-ahead before any bytes have arrived. // ARCHIVE_FILTER_GZIP = 1, ARCHIVE_FORMAT_TAR = 0x30000. - // SAFETY: archive is a valid non-null handle from read_new(); FFI call has no other preconditions. - if unsafe { lib::archive_read_append_filter(archive, 1) } != 0 { + if lib::archive_read_append_filter(a, 1) != 0 { return Err(bun_core::err!("Fail")); } - // SAFETY: archive is a valid non-null handle from read_new(); FFI call has no other preconditions. - if unsafe { lib::archive_read_set_format(archive, 0x30000) } != 0 { + if lib::archive_read_set_format(a, 0x30000) != 0 { return Err(bun_core::err!("Fail")); } - // SAFETY: archive is a valid handle. - let _ = unsafe { (*archive).read_set_options(c"read_concatenated_archives") }; + let _ = a.read_set_options(c"read_concatenated_archives"); // SAFETY: archive is a valid handle; `this` outlives the archive // (freed only in `Drop` after `read_free`). See fn-level # Safety @@ -613,7 +612,7 @@ impl TarballStream { // `&mut self`-derived pointer. let rc_raw: c_int = unsafe { lib::archive_read_open( - archive, + a, this.cast::(), None, Some(archive_read_callback), @@ -644,8 +643,7 @@ impl TarballStream { bun_output::scoped_log!( TarballStream, "archive_read_open: {}", - // SAFETY: archive is a valid handle (guard not yet dropped). - bstr::BStr::new(unsafe { (*archive).error_string() }) + bstr::BStr::new(a.error_string()) ); return Err(bun_core::err!("Fail")); } @@ -927,7 +925,7 @@ impl TarballStream { if block.offset > self.entry_actual_offset { let zero_count: usize = usize::try_from(block.offset - self.entry_actual_offset).expect("int cast"); - match lib::Archive::write_zeros_to_file(file, zero_count) { + match lib::sys::Archive::write_zeros_to_file(file, zero_count) { lib::Result::Ok => { self.entry_actual_offset = block.offset; } @@ -1231,7 +1229,7 @@ fn drain_callback(task: *mut thread_pool::Task) { /// in that setup, so there is no caller-side precondition to encode in the /// signature. The fn-pointer still coerces to the binding's expected type. extern "C" fn archive_read_callback( - _a: *mut lib::Archive, + _a: *mut lib::sys::Archive, ctx: *mut c_void, out_buffer: *mut *const c_void, ) -> lib::la_ssize_t { diff --git a/src/install/extract_tarball.rs b/src/install/extract_tarball.rs index 8604df2180f6..19be538a2770 100644 --- a/src/install/extract_tarball.rs +++ b/src/install/extract_tarball.rs @@ -322,7 +322,7 @@ impl ExtractTarball { && esimated_output_size > 0 { use bun_libdeflate_sys::libdeflate; - if let Some(mut decompressor) = libdeflate::OwnedDecompressor::new() { + if let Some(decompressor) = libdeflate::Decompressor::new() { zlib_pool.list.clear(); let result = decompressor.decompress_to_vec( tgz_bytes, diff --git a/src/install_types/Cargo.toml b/src/install_types/Cargo.toml index 9cfce581c031..a1b2e07f3006 100644 --- a/src/install_types/Cargo.toml +++ b/src/install_types/Cargo.toml @@ -24,3 +24,4 @@ bun_collections.workspace = true bun_ast.workspace = true bun_semver.workspace = true bun_wyhash.workspace = true +bun_opaque.workspace = true diff --git a/src/install_types/NodeLinker.rs b/src/install_types/NodeLinker.rs index 48003db6c87e..d9e90222995b 100644 --- a/src/install_types/NodeLinker.rs +++ b/src/install_types/NodeLinker.rs @@ -73,54 +73,67 @@ pub mod npm { // (`__bun_regex_*`) defined `#[no_mangle]` in `bun_jsc::regular_expression`. // ══════════════════════════════════════════════════════════════════════════ -use core::ptr::NonNull; - use bun_alloc::Arena; use bun_ast as ast; use bun_core::escape_reg_exp::escape_reg_exp_for_package_name_matching; use bun_core::{String as BunString, strings}; -// LAYERING: `bun_jsc::RegularExpression` (Yarr FFI) lives in a higher tier. -// The bodies are defined `#[no_mangle]` in -// `bun_jsc::regular_expression`; declared here as `extern "Rust"` and -// resolved at link time. +// FORWARD_DECL(b0): this tier cannot name `bun_jsc::RegularExpression`, so it +// re-declares the handle. The two ZSTs meet only at the `extern "Rust"` +// boundary below, where both are a bare non-null pointer. +/// The JSC object itself. Only the extern declarations below name this type; +/// all Rust code uses the owning [`RegularExpression`] handle. +pub mod sys { + bun_opaque::opaque_ffi! { + /// `JSC::Yarr::RegularExpression`. `&Self` is ABI-identical to a + /// non-null `RegularExpression*` and carries no `noalias`/`readonly` — + /// Yarr mutates its match state through it. + pub struct RegularExpression; + } +} + +// LAYERING: the bodies live `#[no_mangle]` in the higher-tier +// `bun_jsc::regular_expression`; declared `extern "Rust"`, resolved at link time. +// `&sys::RegularExpression` is ABI-identical to the pointer they take. unsafe extern "Rust" { - /// Compile `pattern` with no flags. `None` ⇔ `error.InvalidRegExp`. + /// Compile `pattern` with no flags. Null ⇔ `error.InvalidRegExp`. /// Performs `jsc::initialize(false)` lazily on first call. - fn __bun_regex_compile(pattern: BunString) -> Option>; - fn __bun_regex_matches(regex: NonNull<()>, input: &BunString) -> bool; - fn __bun_regex_drop(regex: NonNull<()>); + safe fn __bun_regex_compile(pattern: BunString) -> *mut sys::RegularExpression; + safe fn __bun_regex_matches(regex: &sys::RegularExpression, input: &BunString) -> bool; + // safe: runs the Yarr destructor + free. Giving the allocation back is not + // exclusive access, so the receiver is `&`, not `&mut`. + safe fn __bun_regex_drop(regex: &sys::RegularExpression); } -/// Owned, type-erased JSC regex; drops through the vtable. -// FORWARD_DECL(b0): bun_jsc::RegularExpression — stored as raw NonNull<()> -// (NOT Box: a zero-sized opaque Box is a dangling sentinel that would -// leak the real JSC allocation and skip its destructor). -pub struct RegularExpression(NonNull<()>); +// `__bun_regex_compile` allocates and hands back the sole owning pointer. One +// `RegularExpression` handle owns exactly that allocation. +bun_opaque::foreign_handle! { + /// Owned handle to a JSC `Yarr::RegularExpression`. + /// + /// Holds the one ownership unit `__bun_regex_compile` produced; `Drop` gives it + /// back. [`Self::matches`] takes `&self`: Yarr mutates its match state through + /// the same pointer, so there is no `&mut self` to have. + pub struct RegularExpression(sys::RegularExpression) via __bun_regex_drop; +} +/// Matching. `&self`: Yarr mutates through the same pointer. impl RegularExpression { #[inline] pub(crate) fn matches(&self, input: &BunString) -> bool { - // SAFETY: self.0 was produced by `__bun_regex_compile`. - unsafe { __bun_regex_matches(self.0, input) } - } -} - -impl Drop for RegularExpression { - fn drop(&mut self) { - // SAFETY: self.0 was produced by `__bun_regex_compile`; runs JSC destructor + free. - unsafe { __bun_regex_drop(self.0) } + __bun_regex_matches(self.raw(), input) } } -/// Compile `pattern` into a Yarr regex via the link-time extern. `pub` so -/// higher-tier callers re-use this single declaration site instead of -/// duplicating the `__bun_regex_*` extern block (one declarer per upward call, -/// per PORTING.md §extern-Rust-ban). +/// Compile `pattern` into a Yarr regex via the link-time extern. The single +/// declaration site for `__bun_regex_*`, so higher-tier callers do not +/// duplicate the extern block (one declarer per upward call, per PORTING.md +/// §extern-Rust-ban). #[inline] pub(crate) fn compile_regex(pattern: BunString) -> Option { - // SAFETY: link-time extern; pattern ownership transfers. - unsafe { __bun_regex_compile(pattern) }.map(RegularExpression) + // SAFETY: `__bun_regex_compile` leaks a fresh `+1` to us, or returns null on an + // invalid pattern — having already freed the regex it allocated, so there is + // nothing for us to adopt. + unsafe { RegularExpression::adopt_ptr(__bun_regex_compile(pattern)) } } pub struct PnpmMatcher { diff --git a/src/jsc/CachedBytecode.rs b/src/jsc/CachedBytecode.rs index 7daeb0ae614a..0345b29157fe 100644 --- a/src/jsc/CachedBytecode.rs +++ b/src/jsc/CachedBytecode.rs @@ -3,9 +3,27 @@ use core::ptr::NonNull; use bun_core::String as BunString; use bun_options_types::Format; -bun_opaque::opaque_ffi! { - /// Opaque FFI handle to JSC cached bytecode (a C++ `RefPtr` payload). - pub struct CachedBytecode; +/// The C++ object itself. Only the extern declarations below name this type; +/// all Rust code uses the owning [`CachedBytecode`] handle. +pub mod sys { + bun_opaque::opaque_ffi! { + /// `JSC::CachedBytecode`, a `WTF::RefCounted`. `&Self` is ABI-identical + /// to a non-null `JSC::CachedBytecode*` and carries no + /// `noalias`/`readonly` — C++ mutates the refcount through it. + pub struct CachedBytecode; + } +} + +// `JSC::encodeCodeBlock` allocates and the C++ generator does an explicit +// `->ref()` before leaking the pointer through the out-param, so Rust receives +// a `+1`. One `CachedBytecode` handle owns exactly that one ref. +bun_opaque::foreign_handle! { + /// Owned handle to a C++ `JSC::CachedBytecode`. + /// + /// Holds one ref on the WTF intrusive refcount; `Drop` gives it back, freeing + /// the bytecode buffer at zero. There is no `&mut self` and no `DerefMut`: a + /// refcount is shared by definition, and a decrement is not exclusive access. + pub struct CachedBytecode(sys::CachedBytecode) via CachedBytecode__deref; } unsafe extern "C" { @@ -15,7 +33,7 @@ unsafe extern "C" { input_source_code_size: usize, output_byte_code: *mut Option>, output_byte_code_size: *mut usize, - cached_bytecode: *mut Option>, + cached_bytecode: *mut Option>, ) -> bool; fn generateCachedCommonJSProgramByteCodeFromSourceCode( @@ -24,24 +42,25 @@ unsafe extern "C" { input_source_code_size: usize, output_byte_code: *mut Option>, output_byte_code_size: *mut usize, - cached_bytecode: *mut Option>, + cached_bytecode: *mut Option>, ) -> bool; - // safe: `CachedBytecode` is an `opaque_ffi!` ZST handle (`!Freeze` via - // `UnsafeCell`); `&mut` is ABI-identical to a non-null `*mut` and the C++ - // refcount decrement is interior to the cell. - safe fn CachedBytecode__deref(this: &mut CachedBytecode); + // safe: C++ takes `CachedBytecode*` and calls the intrusive `->deref()`. A + // refcount decrement is not exclusive access — other refs exist by + // definition — so the receiver is `&`, not `&mut`. + safe fn CachedBytecode__deref(this: &sys::CachedBytecode); } +/// Bytecode generation. Each successful call returns the `+1` C++ handed us. impl CachedBytecode { // SAFETY CONTRACT: the returned `&'static [u8]` actually borrows from the - // `CachedBytecode` handle and is invalidated when `deref()` is called. Callers own - // the handle and must call `deref()` (or drop via `allocator()`) to free. + // `CachedBytecode` handle and is invalidated when that handle is dropped. + // Callers must keep it alive for as long as they read the slice. pub fn generate_for_esm( source_provider_url: &mut BunString, input: &[u8], - ) -> Option<(&'static [u8], NonNull)> { - let mut this: Option> = None; + ) -> Option<(&'static [u8], Self)> { + let mut this: Option> = None; let mut input_code_size: usize = 0; let mut input_code_ptr: Option> = None; @@ -58,10 +77,15 @@ impl CachedBytecode { }; if ok { // SAFETY: on success, C++ guarantees both out-params are non-null - // and the slice is valid for `input_code_size` bytes until deref(). + // and the slice is valid for `input_code_size` bytes until release. let slice = unsafe { bun_core::ffi::slice(input_code_ptr.unwrap().as_ptr(), input_code_size) }; - return Some((slice, this.unwrap())); + let ptr = this.map_or(core::ptr::null_mut(), |p| p.as_ptr()); + // SAFETY: the C++ generator `->ref()`s before writing the out-param, + // transferring that `+1` to us; no other handle will release it. + let handle = + unsafe { Self::adopt_ptr(ptr) }.expect("bytecode generated but handle is null"); + return Some((slice, handle)); } None @@ -70,8 +94,8 @@ impl CachedBytecode { pub fn generate_for_cjs( source_provider_url: &mut BunString, input: &[u8], - ) -> Option<(&'static [u8], NonNull)> { - let mut this: Option> = None; + ) -> Option<(&'static [u8], Self)> { + let mut this: Option> = None; let mut input_code_size: usize = 0; let mut input_code_ptr: Option> = None; // SAFETY: out-params are valid for write; input slice valid for read. @@ -87,24 +111,25 @@ impl CachedBytecode { }; if ok { // SAFETY: on success, C++ guarantees both out-params are non-null - // and the slice is valid for `input_code_size` bytes until deref(). + // and the slice is valid for `input_code_size` bytes until release. let slice = unsafe { bun_core::ffi::slice(input_code_ptr.unwrap().as_ptr(), input_code_size) }; - return Some((slice, this.unwrap())); + let ptr = this.map_or(core::ptr::null_mut(), |p| p.as_ptr()); + // SAFETY: the C++ generator `->ref()`s before writing the out-param, + // transferring that `+1` to us; no other handle will release it. + let handle = + unsafe { Self::adopt_ptr(ptr) }.expect("bytecode generated but handle is null"); + return Some((slice, handle)); } None } - pub fn deref(&mut self) { - CachedBytecode__deref(self) - } - pub fn generate( format: Format, input: &[u8], source_provider_url: &mut BunString, - ) -> Option<(&'static [u8], NonNull)> { + ) -> Option<(&'static [u8], Self)> { match format { Format::Esm => Self::generate_for_esm(source_provider_url, input), Format::Cjs => Self::generate_for_cjs(source_provider_url, input), @@ -113,24 +138,6 @@ impl CachedBytecode { } } -// ────────────────────────────────────────────────────────────────────────── -// The `bun_alloc::Allocator` marker trait has no -// `alloc`/`free` methods to dispatch through — so "free → deref" semantics -// cannot ride the trait object. Call sites that would have freed through this -// allocator must instead call `deref()` on the `NonNull` handle -// directly. `is_instance` is preserved for the vtable-identity check in -// `bun_safety::alloc::has_ptr`. -// ────────────────────────────────────────────────────────────────────────── - -impl bun_alloc::Allocator for CachedBytecode {} - -impl CachedBytecode { - /// Concrete-type identity check via the `Allocator::type_id()` hook. - pub fn is_instance(alloc: &dyn bun_alloc::Allocator) -> bool { - alloc.is::() - } -} - /// Link-time entry point for lower-tier crates (declared `extern "Rust"` in /// `bun_bundler`). Generic "generate JSC bytecode off the main JS thread" /// helper: marks the calling thread as a bytecode-only thread (so WTF timer @@ -150,9 +157,8 @@ pub(crate) fn __bun_jsc_generate_cached_bytecode( crate::initialize(false); let (bytes, handle) = CachedBytecode::generate(format, source, source_provider_url)?; let owned = Box::<[u8]>::from(bytes); - // `handle` was just produced by C++ and is valid until deref; - // `CachedBytecode` is an opaque ZST handle so `opaque_mut` is the - // centralised zero-byte deref proof. - CachedBytecode__deref(CachedBytecode::opaque_mut(handle.as_ptr())); + // `bytes` borrows the C++ buffer; the copy above is done, so give the ref + // back. Dropping `handle` is the release. + drop(handle); Some(owned) } diff --git a/src/jsc/CppTask.rs b/src/jsc/CppTask.rs index 130402469c25..877b4e814dc4 100644 --- a/src/jsc/CppTask.rs +++ b/src/jsc/CppTask.rs @@ -4,37 +4,54 @@ use crate::{JSGlobalObject, JsResult, VirtualMachineRef as VirtualMachine}; use bun_event_loop::{TaskTag, Taskable, task_tag}; use bun_threading::work_pool::{Task as WorkPoolTask, WorkPool}; +/// The C++ object itself. Only the extern declarations below name this type; +/// all Rust code uses the owning [`CppTask`] handle. +pub mod sys { + bun_opaque::opaque_ffi! { + /// `WebCore::EventLoopTask`. `&Self` is ABI-identical to a non-null + /// `WebCore::EventLoopTask*`, and carries no `noalias`/`readonly` — + /// C++ runs and destroys the captured `WTF::Function` through it. + pub struct CppTask; + } +} + #[allow(improper_ctypes)] // VirtualMachine is opaque to C++; passed as `void*` unsafe extern "C" { fn Bun__EventLoopTaskNoContext__performTask(task: *mut EventLoopTaskNoContext); safe fn Bun__EventLoopTaskNoContext__createdInBunVm( task: &EventLoopTaskNoContext, ) -> *mut VirtualMachine; + // safe: C++ `delete task`, without running it. Destroying is not exclusive + // access in Rust's sense, so the receiver is `&`, matching `UnsafeCell`. + safe fn Bun__deleteEventLoopTask(task: &sys::CppTask); } -bun_opaque::opaque_ffi! { - /// A task created from C++ code, usually via ScriptExecutionContext. - pub struct CppTask; +// The tag describes the C++ pointee stored in `Task.ptr`, not the Rust handle. +impl Taskable for sys::CppTask { + const TAG: TaskTag = task_tag::CppTask; } -impl Taskable for CppTask { - const TAG: TaskTag = task_tag::CppTask; +// C++ `new EventLoopTask` (`ScriptExecutionContext::postTask*`) hands Rust the +// sole owner. `delete` gives it back; so does `performTask` (`delete this`). +bun_opaque::foreign_handle! { + /// Owned handle to a C++ `WebCore::EventLoopTask` — a task posted from C++, + /// usually via `ScriptExecutionContext`. + /// + /// Holds the sole owner of the heap task; `Drop` deletes it without running. + /// [`Self::run`] takes `self` instead: `performTask` does `delete this`. + pub struct CppTask(sys::CppTask) via Bun__deleteEventLoopTask; } impl CppTask { - pub fn run(&mut self, global: &JSGlobalObject) -> JsResult<()> { + /// Run the task. Consumes `self`: C++ `performTask` does `delete this`, + /// on the throwing path too, so the owner is given back exactly once. + pub fn run(self, global: &JSGlobalObject) -> JsResult<()> { crate::mark_binding!(); - // SAFETY: self is a valid C++ EventLoopTask; global outlives the call. - // - // `Bun__performTask` is `[[ZIG_EXPORT(check_slow)]]` — the task body - // (a `ScriptExecutionContext::postTask` lambda) may declare its own - // throw scope (e.g. `JSUint8Array::create`, `JSC::call`) without an - // enclosing one, so we must go through the generated `cpp::` wrapper - // (which opens a `TopExceptionScope` and `return_if_exception`s) rather - // than the raw FFI. Calling the raw extern left the simulated throw - // unchecked, which then tripped `drainMicrotasks`'s scope ctor under - // `BUN_JSC_validateExceptionChecks=1`. - unsafe { crate::cpp::Bun__performTask(global, std::ptr::from_mut::(self)) } + let task = self.leak(); + // The task body may open a throw scope with no enclosing one, so go through + // the generated `cpp::` wrapper, which opens a `TopExceptionScope`. + // SAFETY: `task` is the live owner we just gave up; C++ deletes it. + unsafe { crate::cpp::Bun__performTask(global, task.as_ptr().cast()) } } } diff --git a/src/jsc/DOMFormData.rs b/src/jsc/DOMFormData.rs index bc406d3bb3a5..bb952accc03a 100644 --- a/src/jsc/DOMFormData.rs +++ b/src/jsc/DOMFormData.rs @@ -14,36 +14,32 @@ unsafe extern "C" { arg0: &JSGlobalObject, arg1: &ZigString, ) -> JSValue; - // safe: `DOMFormData` is an `opaque_ffi!` ZST handle (`&mut` is ABI-identical - // to a non-null `*mut`); `arg1` is an opaque round-trip pointer C++ only - // forwards to `arg2` (synchronous, never retained or dereferenced as Rust data). + // safe: `DOMFormData` is an `opaque_ffi!` ZST handle (`&` is ABI-identical to a + // non-null pointer and carries no `noalias`/`readonly`); `arg1` is an opaque + // round-trip pointer C++ only forwards to `arg2` (synchronous, never retained). safe fn WebCore__DOMFormData__toQueryString( - arg0: &mut DOMFormData, + arg0: &DOMFormData, arg1: *mut c_void, arg2: extern "C" fn(arg0: *mut c_void, arg1: *mut ZigString), ); safe fn WebCore__DOMFormData__fromJS(js_value0: JSValue) -> *mut DOMFormData; - safe fn WebCore__DOMFormData__append( - arg0: &mut DOMFormData, - arg1: &ZigString, - arg2: &ZigString, - ); + safe fn WebCore__DOMFormData__append(arg0: &DOMFormData, arg1: &ZigString, arg2: &ZigString); // safe: `DOMFormData`/`JSGlobalObject` are opaque `UnsafeCell`-backed ZST // handles; `&ZigString` is ABI-identical to non-null `*const ZigString` and // C++ only reads the named struct via `toStringCopy`. `arg3` is an opaque // `*Blob` C++ owns (never dereferenced as Rust data) — same round-trip // contract as `Zig__GlobalObject__resetModuleRegistryMap`'s `map` param. safe fn WebCore__DOMFormData__appendBlob( - arg0: &mut DOMFormData, + arg0: &DOMFormData, arg1: &JSGlobalObject, arg2: &ZigString, arg3: *mut c_void, arg4: &ZigString, ); - safe fn WebCore__DOMFormData__count(arg0: &mut DOMFormData) -> usize; + safe fn WebCore__DOMFormData__count(arg0: &DOMFormData) -> usize; // safe: same opaque-handle/round-trip-ctx contract as `toQueryString` above. - safe fn DOMFormData__forEach(this: &mut DOMFormData, ctx: *mut c_void, cb: ForEachFunction); + safe fn DOMFormData__forEach(this: &DOMFormData, ctx: *mut c_void, cb: ForEachFunction); } impl DOMFormData { @@ -63,7 +59,7 @@ impl DOMFormData { // The closure environment is the ctx pointer; the generic trampoline // below unwraps it and invokes the closure. - pub fn to_query_string(&mut self, callback: &mut F) + pub fn to_query_string(&self, callback: &mut F) where F: FnMut(ZigString), { @@ -83,23 +79,20 @@ impl DOMFormData { ); } - pub fn from_js<'a>(value: JSValue) -> Option<&'a mut DOMFormData> { - // Returned pointer is valid while `value` is kept alive on the stack - // (conservative GC scan). Null → None. `DOMFormData` is an opaque ZST - // handle, so `opaque_mut` is the centralised zero-byte deref proof. - // The unbounded `'a` cannot be expressed more tightly: the cell is - // GC-owned, so the caller must keep `value` stack-rooted for the - // lifetime of the returned reference. + pub fn from_js<'a>(value: JSValue) -> Option<&'a DOMFormData> { + // Valid while `value` stays stack-rooted (conservative GC scan); null → None. + // `opaque_ref` is the centralised zero-byte deref proof. `'a` is unbounded + // because the cell is GC-owned, not borrowed from `value`. let p = WebCore__DOMFormData__fromJS(value); - (!p.is_null()).then(|| DOMFormData::opaque_mut(p)) + (!p.is_null()).then(|| DOMFormData::opaque_ref(p)) } - pub fn append(&mut self, name_: &ZigString, value_: &ZigString) { + pub fn append(&self, name_: &ZigString, value_: &ZigString) { WebCore__DOMFormData__append(self, name_, value_) } pub fn append_blob( - &mut self, + &self, global: &JSGlobalObject, name_: &ZigString, blob: *mut c_void, @@ -108,7 +101,7 @@ impl DOMFormData { WebCore__DOMFormData__appendBlob(self, global, name_, blob, filename_); } - pub fn count(&mut self) -> usize { + pub fn count(&self) -> usize { WebCore__DOMFormData__count(self) } @@ -117,7 +110,7 @@ impl DOMFormData { // hands it as `*mut c_void`; this fn is generic over `B` so the caller (in // `bun_runtime`) names the concrete `Blob` type and gets a typed `&B` // borrow without `bun_jsc` ever seeing the layout. - pub fn for_each(&mut self, callback: &mut F) + pub fn for_each(&self, callback: &mut F) where F: FnMut(ZigString, FormDataEntry<'_, B>), { diff --git a/src/jsc/DOMURL.rs b/src/jsc/DOMURL.rs index 04a8bfe2e9db..0c1c2dbdbe04 100644 --- a/src/jsc/DOMURL.rs +++ b/src/jsc/DOMURL.rs @@ -31,17 +31,18 @@ pub enum ToFileSystemPathError { bun_core::named_error_set!(ToFileSystemPathError); impl DOMURL { - pub fn cast_<'a>(value: JSValue, vm: &'a VM) -> Option<&'a mut DOMURL> { + pub fn cast_<'a>(value: JSValue, vm: &'a VM) -> Option<&'a DOMURL> { // DOMURL is a GC-owned C++ cell; the returned reference is only valid // while `value` stays alive (e.g. stack-rooted for the conservative GC // scan) — the borrow on `vm` does not capture that. - // `DOMURL` is an `opaque_ffi!` ZST handle; `opaque_mut` is the - // centralised non-null-ZST deref proof (zero-byte `&mut` cannot alias). + // `DOMURL` is an `opaque_ffi!` ZST handle; `opaque_ref` is the + // centralised non-null-ZST deref proof. `&DOMURL` is `!Freeze`: no + // `noalias`/`readonly`, so C++ may mutate the cell behind it. let p = WebCore__DOMURL__cast_(value, vm); - (!p.is_null()).then(|| DOMURL::opaque_mut(p)) + (!p.is_null()).then(|| DOMURL::opaque_ref(p)) } - pub fn cast<'a>(value: JSValue) -> Option<&'a mut DOMURL> { + pub fn cast<'a>(value: JSValue) -> Option<&'a DOMURL> { // SAFETY: VirtualMachine::get() returns the per-thread singleton; caller is on the JS thread. Self::cast_( value, @@ -49,17 +50,17 @@ impl DOMURL { ) } - pub fn href_(&mut self, out: &mut ZigString) { + pub fn href_(&self, out: &mut ZigString) { WebCore__DOMURL__href_(self, out) } - pub fn href(&mut self) -> ZigString { + pub fn href(&self) -> ZigString { let mut out = ZigString::EMPTY; self.href_(&mut out); out } - pub fn file_system_path(&mut self) -> Result { + pub fn file_system_path(&self) -> Result { let mut error_code: c_int = 0; let path = WebCore__DOMURL__fileSystemPath(self, &mut error_code); match error_code { @@ -72,11 +73,11 @@ impl DOMURL { Ok(path) } - pub fn pathname_(&mut self, out: &mut ZigString) { + pub fn pathname_(&self, out: &mut ZigString) { WebCore__DOMURL__pathname_(self, out) } - pub fn pathname(&mut self) -> ZigString { + pub fn pathname(&self) -> ZigString { let mut out = ZigString::EMPTY; self.pathname_(&mut out); out diff --git a/src/jsc/Debugger.rs b/src/jsc/Debugger.rs index 2e264107da84..be9d4922a388 100644 --- a/src/jsc/Debugger.rs +++ b/src/jsc/Debugger.rs @@ -731,7 +731,7 @@ bun_opaque::opaque_ffi! { pub struct TestReporterHandle; } // by-value scalars. unsafe extern "C" { safe fn Bun__TestReporterAgentReportTestFound( - agent: &mut TestReporterHandle, + agent: &TestReporterHandle, call_frame: &CallFrame, test_id: c_int, name: &mut BunString, @@ -739,7 +739,7 @@ unsafe extern "C" { parent_id: c_int, ); safe fn Bun__TestReporterAgentReportTestFoundWithLocation( - agent: &mut TestReporterHandle, + agent: &TestReporterHandle, test_id: c_int, name: &mut BunString, item_type: TestType, @@ -747,9 +747,9 @@ unsafe extern "C" { source_url: &mut BunString, line: c_int, ); - safe fn Bun__TestReporterAgentReportTestStart(agent: &mut TestReporterHandle, test_id: c_int); + safe fn Bun__TestReporterAgentReportTestStart(agent: &TestReporterHandle, test_id: c_int); safe fn Bun__TestReporterAgentReportTestEnd( - agent: &mut TestReporterHandle, + agent: &TestReporterHandle, test_id: c_int, bun_test_status: TestStatus, elapsed: f64, @@ -758,7 +758,7 @@ unsafe extern "C" { impl TestReporterHandle { pub fn report_test_found( - &mut self, + &self, call_frame: &CallFrame, test_id: i32, name: &mut BunString, @@ -771,7 +771,7 @@ impl TestReporterHandle { } pub fn report_test_found_with_location( - &mut self, + &self, test_id: i32, name: &mut BunString, item_type: TestType, @@ -784,11 +784,11 @@ impl TestReporterHandle { ); } - pub fn report_test_start(&mut self, test_id: c_int) { + pub fn report_test_start(&self, test_id: c_int) { Bun__TestReporterAgentReportTestStart(self, test_id); } - pub fn report_test_end(&mut self, test_id: c_int, bun_test_status: TestStatus, elapsed: f64) { + pub fn report_test_end(&self, test_id: c_int, bun_test_status: TestStatus, elapsed: f64) { Bun__TestReporterAgentReportTestEnd(self, test_id, bun_test_status, elapsed); } } @@ -827,16 +827,16 @@ pub fn test_reporter_agent_disable(_agent: *mut TestReporterHandle) { } impl TestReporterAgent { - /// Safe `&mut TestReporterHandle` accessor — `handle` is a live C++ + /// Safe `&TestReporterHandle` accessor — `handle` is a live C++ /// `Inspector::TestReporterAgent*` once the agent is enabled. Caller must /// ensure `is_enabled()` (handle != null). #[inline] - fn handle_mut(&mut self) -> &mut TestReporterHandle { + fn handle_ref(&self) -> &TestReporterHandle { debug_assert!(!self.handle.is_null()); // Caller contract — `is_enabled()` checked; handle is a live C++ heap // allocation owned by the inspector backend. `TestReporterHandle` is an - // opaque ZST handle so the deref is the centralised `opaque_mut` proof. - TestReporterHandle::opaque_mut(self.handle) + // opaque ZST handle so the deref is the centralised `opaque_ref` proof. + TestReporterHandle::opaque_ref(self.handle) } /// Caller must ensure that it is enabled first. @@ -851,20 +851,20 @@ impl TestReporterAgent { parent_id: i32, ) { bun_core::scoped_log!(TestReporterAgent, "reportTestFound"); - self.handle_mut() + self.handle_ref() .report_test_found(call_frame, test_id, name, item_type, parent_id); } /// Caller must ensure that it is enabled first. pub fn report_test_start(&mut self, test_id: i32) { bun_core::scoped_log!(TestReporterAgent, "reportTestStart"); - self.handle_mut().report_test_start(test_id); + self.handle_ref().report_test_start(test_id); } /// Caller must ensure that it is enabled first. pub fn report_test_end(&mut self, test_id: i32, bun_test_status: TestStatus, elapsed: f64) { bun_core::scoped_log!(TestReporterAgent, "reportTestEnd"); - self.handle_mut() + self.handle_ref() .report_test_end(test_id, bun_test_status, elapsed); } @@ -886,30 +886,27 @@ bun_opaque::opaque_ffi! { pub struct LifecycleHandle; } // via `UnsafeCell`); `ZigException` is a `#[repr(C)]` out-param the C++ side // reads/fills in-place. unsafe extern "C" { - safe fn Bun__LifecycleAgentReportReload(agent: &mut LifecycleHandle); - safe fn Bun__LifecycleAgentReportError( - agent: &mut LifecycleHandle, - exception: &mut ZigException, - ); - safe fn Bun__LifecycleAgentPreventExit(agent: &mut LifecycleHandle); - safe fn Bun__LifecycleAgentStopPreventingExit(agent: &mut LifecycleHandle); + safe fn Bun__LifecycleAgentReportReload(agent: &LifecycleHandle); + safe fn Bun__LifecycleAgentReportError(agent: &LifecycleHandle, exception: &mut ZigException); + safe fn Bun__LifecycleAgentPreventExit(agent: &LifecycleHandle); + safe fn Bun__LifecycleAgentStopPreventingExit(agent: &LifecycleHandle); } impl LifecycleHandle { - pub fn prevent_exit(&mut self) { + pub fn prevent_exit(&self) { Bun__LifecycleAgentPreventExit(self) } - pub fn stop_preventing_exit(&mut self) { + pub fn stop_preventing_exit(&self) { Bun__LifecycleAgentStopPreventingExit(self) } - pub fn report_reload(&mut self) { + pub fn report_reload(&self) { bun_core::scoped_log!(LifecycleAgent, "reportReload"); Bun__LifecycleAgentReportReload(self) } - pub fn report_error(&mut self, exception: &mut ZigException) { + pub fn report_error(&self, exception: &mut ZigException) { bun_core::scoped_log!(LifecycleAgent, "reportError"); Bun__LifecycleAgentReportError(self, exception) } @@ -938,15 +935,15 @@ pub fn lifecycle_agent_disable(_agent: *mut LifecycleHandle) { impl LifecycleAgent { /// Safe optional accessor — wraps the null check + raw deref. #[inline] - fn handle_mut(&mut self) -> Option<&mut LifecycleHandle> { + fn handle_ref(&self) -> Option<&LifecycleHandle> { // `handle` is null or a live C++ heap allocation owned by the inspector // backend. `LifecycleHandle` is an opaque ZST handle so the deref is - // the centralised `opaque_mut` proof. - core::ptr::NonNull::new(self.handle).map(|p| LifecycleHandle::opaque_mut(p.as_ptr())) + // the centralised `opaque_ref` proof. + core::ptr::NonNull::new(self.handle).map(|p| LifecycleHandle::opaque_ref(p.as_ptr())) } pub(crate) fn report_error(&mut self, exception: &mut ZigException) { - if let Some(h) = self.handle_mut() { + if let Some(h) = self.handle_ref() { h.report_error(exception); } } diff --git a/src/jsc/FetchHeaders.rs b/src/jsc/FetchHeaders.rs index f15eb22a8e86..34ccfb0ccf1e 100644 --- a/src/jsc/FetchHeaders.rs +++ b/src/jsc/FetchHeaders.rs @@ -20,18 +20,17 @@ pub mod sys { // C++ allocates (`new WebCore::FetchHeaders` + `relaxAdoptionRequirement`) and // hands back a `+1`. One `FetchHeaders` handle owns exactly that one ref. -bun_opaque::foreign_owned!(sys::FetchHeaders, WebCore__FetchHeaders__deref); - -/// Owned handle to a C++ `WebCore::FetchHeaders`. -/// -/// Holds one ref on the C++ intrusive refcount; `Drop` gives it back. Every -/// method takes `&self`: a refcount is shared by definition, and C++ mutates -/// the headers through the same pointer, so there is no `&mut self` to have. -/// -/// A `FetchHeaders` *borrowed* from a JS `Headers` wrapper (see [`Self::cast`]) -/// is a `ManuallyDrop` — the JS object owns that ref, not us. -#[repr(transparent)] -pub struct FetchHeaders(bun_opaque::ForeignRef); +bun_opaque::foreign_handle! { + /// Owned handle to a C++ `WebCore::FetchHeaders`. + /// + /// Holds one ref on the C++ intrusive refcount; `Drop` gives it back. Every + /// method takes `&self`: a refcount is shared by definition, and C++ mutates + /// the headers through the same pointer, so there is no `&mut self` to have. + /// + /// A `FetchHeaders` *borrowed* from a JS `Headers` wrapper (see [`Self::cast`]) + /// is a `ManuallyDrop` — the JS object owns that ref, not us. + pub struct FetchHeaders(sys::FetchHeaders) via WebCore__FetchHeaders__deref; +} // `JSGlobalObject`/`VM`/`sys::FetchHeaders` are opaque `UnsafeCell`-backed ZST // handles, so `&T` is ABI-identical to a non-null `*const T` and C++ mutating @@ -139,63 +138,27 @@ struct PicoHeaders { len: usize, } -/// Ownership plumbing. -impl FetchHeaders { - /// Adopt a `+1` returned by C++. - /// - /// # Safety - /// `ptr` must carry exactly one ref that no other handle will release. - #[inline] - pub unsafe fn adopt(ptr: NonNull) -> Self { - // SAFETY: caller transfers the +1. - Self(unsafe { bun_opaque::ForeignRef::adopt(ptr) }) - } - - /// Adopt a nullable `+1`; `None` on null. - #[inline] - fn adopt_ptr(ptr: *mut sys::FetchHeaders) -> Option { - // SAFETY: C++ `create*` returns a fresh +1 or null. - NonNull::new(ptr).map(|p| unsafe { Self::adopt(p) }) - } - - /// The C++ pointer, still owned by `self`. - #[inline] - pub fn as_ptr(&self) -> *mut sys::FetchHeaders { - self.0.as_ptr() - } - - /// Hand our `+1` to a foreign owner. Pairs with a later [`Self::adopt`]. - #[inline] - pub fn leak(self) -> NonNull { - self.0.leak() - } - - #[inline] - fn raw(&self) -> &sys::FetchHeaders { - &self.0 - } -} - /// Constructors. C++ allocates; every one of these returns a `+1`. impl FetchHeaders { pub fn create_empty() -> Self { - Self::adopt_ptr(WebCore__FetchHeaders__createEmpty()) + // SAFETY: C++ `createEmpty` transfers a fresh `+1`, or returns null. + unsafe { Self::adopt_ptr(WebCore__FetchHeaders__createEmpty()) } .expect("WebCore__FetchHeaders__createEmpty returned null") } /// # Safety /// `uws_request` must be a live `uWS::HttpRequest*`; C++ dereferences it. pub unsafe fn create_from_uws(uws_request: *mut c_void) -> Self { - // SAFETY: caller contract. - Self::adopt_ptr(unsafe { WebCore__FetchHeaders__createFromUWS(uws_request) }) + // SAFETY: caller contract; C++ `createFromUWS` transfers a fresh `+1`, or null. + unsafe { Self::adopt_ptr(WebCore__FetchHeaders__createFromUWS(uws_request)) } .expect("WebCore__FetchHeaders__createFromUWS returned null") } /// # Safety /// `h3_request` must be a live `uWS::Http3Request*`; C++ dereferences it. pub unsafe fn create_from_h3(h3_request: *mut c_void) -> Self { - // SAFETY: caller contract. - Self::adopt_ptr(unsafe { WebCore__FetchHeaders__createFromH3(h3_request) }) + // SAFETY: caller contract; C++ `createFromH3` transfers a fresh `+1`, or null. + unsafe { Self::adopt_ptr(WebCore__FetchHeaders__createFromH3(h3_request)) } .expect("WebCore__FetchHeaders__createFromH3 returned null") } @@ -212,8 +175,8 @@ impl FetchHeaders { /// # Safety /// `pico_headers` must point to a live `PicoHeaders`. unsafe fn create_from_pico_headers_(pico_headers: *const c_void) -> Self { - // SAFETY: caller contract. - Self::adopt_ptr(unsafe { WebCore__FetchHeaders__createFromPicoHeaders_(pico_headers) }) + // SAFETY: caller contract; C++ `createFromPicoHeaders_` transfers a fresh `+1`, or null. + unsafe { Self::adopt_ptr(WebCore__FetchHeaders__createFromPicoHeaders_(pico_headers)) } .expect("WebCore__FetchHeaders__createFromPicoHeaders_ returned null") } @@ -221,7 +184,8 @@ impl FetchHeaders { /// `Record`. Throws on invalid input; `None` if empty. pub fn create_from_js(global: &JSGlobalObject, value: JSValue) -> JsResult> { host_fn::from_js_host_call_generic(global, || { - Self::adopt_ptr(WebCore__FetchHeaders__createFromJS(global, value)) + // SAFETY: C++ `createFromJS` transfers a fresh `+1`, or returns null. + unsafe { Self::adopt_ptr(WebCore__FetchHeaders__createFromJS(global, value)) } }) } @@ -246,13 +210,15 @@ impl FetchHeaders { count, ) }; - Self::adopt_ptr(p) + // SAFETY: C++ `createValueNotJS` transfers a fresh `+1`, or returns null. + unsafe { Self::adopt_ptr(p) } } /// Deep-copies on the C++ side, so the result is a fresh `+1`. pub fn clone_this(&self, global: &JSGlobalObject) -> JsResult> { host_fn::from_js_host_call_generic(global, || { - Self::adopt_ptr(WebCore__FetchHeaders__cloneThis(self.raw(), global)) + // SAFETY: C++ `cloneThis` deep-copies and transfers a fresh `+1`, or returns null. + unsafe { Self::adopt_ptr(WebCore__FetchHeaders__cloneThis(self.raw(), global)) } }) } diff --git a/src/jsc/JSCScheduler.rs b/src/jsc/JSCScheduler.rs index e17cd9523656..779308f2ce5b 100644 --- a/src/jsc/JSCScheduler.rs +++ b/src/jsc/JSCScheduler.rs @@ -16,13 +16,13 @@ impl Taskable for JSCDeferredWorkTask { unsafe extern "C" { // safe: `JSCDeferredWorkTask` is an `opaque_ffi!` ZST handle (`!Freeze` - // via `UnsafeCell`); `&mut` is ABI-identical to a non-null `*mut` and the - // C++ side consuming it is interior to the opaque cell. - safe fn Bun__runDeferredWork(task: &mut JSCDeferredWorkTask); + // via `UnsafeCell`); `&T` is ABI-identical to a non-null pointer and the + // C++ side consuming it mutates interior to the opaque cell. + safe fn Bun__runDeferredWork(task: &JSCDeferredWorkTask); } impl JSCDeferredWorkTask { - pub fn run(&mut self) -> Result<(), JsTerminated> { + pub fn run(&self) -> Result<(), JsTerminated> { // SAFETY: `VirtualMachine::get()` returns the live per-thread VM; `global` is // initialized during VM startup and remains valid for the VM's lifetime. let global_this = VirtualMachine::get().global(); diff --git a/src/jsc/JSObject.rs b/src/jsc/JSObject.rs index 041906c343ee..c0ed5608d369 100644 --- a/src/jsc/JSObject.rs +++ b/src/jsc/JSObject.rs @@ -53,10 +53,7 @@ impl JSObject { /// /// This method is equivalent to `Object.create(...)` + setting properties, /// and is only intended for creating POJOs. - pub fn create( - pojo: &T, - global: &JSGlobalObject, - ) -> JsResult<&'static mut JSObject> { + pub fn create(pojo: &T, global: &JSGlobalObject) -> JsResult<&'static JSObject> { Self::create_from_struct_with_prototype::(pojo, global) } @@ -71,7 +68,7 @@ impl JSObject { pub fn create_null_proto( pojo: &T, global: &JSGlobalObject, - ) -> JsResult<&'static mut JSObject> { + ) -> JsResult<&'static JSObject> { Self::create_from_struct_with_prototype::(pojo, global) } @@ -90,7 +87,7 @@ impl JSObject { fn create_from_struct_with_prototype( pojo: &T, global: &JSGlobalObject, - ) -> JsResult<&'static mut JSObject> { + ) -> JsResult<&'static JSObject> { // Rust has no field reflection; `PojoFields` impls are hand-written // (see trait docs) and emit an inline // `put(b"name", JSValue::from_any(global, &self.name)?)?;` per field. @@ -101,12 +98,10 @@ impl JSObject { JSValue::create_empty_object(global, T::FIELD_COUNT) }; debug_assert!(val.is_object()); - // `val.is_object()` asserted above in debug; JSC guarantees these - // constructors return a JSObject cell. A cell-tagged JSValue's payload - // IS the cell pointer (NotCellMask bits are zero). `JSObject` is an - // `opaque_ffi!` ZST handle; `opaque_mut` is the centralised - // non-null-ZST deref proof (zero-byte `&mut` cannot alias). - let obj = JSObject::opaque_mut(val.0 as *mut JSObject); + // JSC guarantees these constructors return a JSObject cell; a cell-tagged + // JSValue's payload IS the cell pointer (NotCellMask bits are zero). + // `opaque_ref` is the centralised non-null-ZST deref proof. + let obj = JSObject::opaque_ref(val.0 as *const JSObject); let cell = obj.to_js(); // Each `fromAny` result is `put()` immediately before the next field @@ -213,7 +208,7 @@ impl JSObject { #[track_caller] pub fn put_record( - &mut self, + &self, global: &JSGlobalObject, key: &mut ZigString, values: &mut [ZigString], @@ -232,7 +227,7 @@ impl JSObject { } /// This will not call getters or be observable from JavaScript. - pub fn get_code_property_vm_inquiry(&mut self, global: &JSGlobalObject) -> Option { + pub fn get_code_property_vm_inquiry(&self, global: &JSGlobalObject) -> Option { let v = Bun__JSObject__getCodePropertyVMInquiry(global, self); if v.is_empty() { return None; @@ -286,7 +281,7 @@ pub(crate) type InitializeCallback = /// Object-initializer contract: implement `create` on your context type and /// pass it to `JSObject::create_with_initializer`. pub trait ObjectInitializer { - fn create(&mut self, obj: &mut JSObject, global: &JSGlobalObject) -> JsResult<()>; + fn create(&mut self, obj: &JSObject, global: &JSGlobalObject) -> JsResult<()>; } extern "C" fn initializer_call( @@ -297,7 +292,7 @@ extern "C" fn initializer_call( // SAFETY: `this` was produced from `&mut Ctx` in `create_with_initializer`; // `obj` is a live JSC pointer for the duration of the callback. `global` is // taken by reference at the C ABI (`&T` ≡ non-null `*const T`). - let result = unsafe { Ctx::create(&mut *this.cast::(), &mut *obj, global) }; + let result = unsafe { Ctx::create(&mut *this.cast::(), &*obj, global) }; if let Err(err) = result { // Mirrors `host_fn::void_from_js_error` — OOM throws, // anything else asserts an exception is already pending. diff --git a/src/jsc/JSPromise.rs b/src/jsc/JSPromise.rs index 54a947eafd66..ba4a8b1bb06a 100644 --- a/src/jsc/JSPromise.rs +++ b/src/jsc/JSPromise.rs @@ -154,12 +154,12 @@ impl Strong { pub fn reject_without_swap(&mut self, global: &JSGlobalObject, val: JsResult) { let Some(v) = self.strong.get() else { return }; let val = val.unwrap_or_else(|_| global.try_take_exception().unwrap()); - let _ = JSPromise::opaque_mut(v.as_promise().unwrap()).reject(global, Ok(val)); + let _ = JSPromise::opaque_ref(v.as_promise().unwrap()).reject(global, Ok(val)); } pub fn resolve_without_swap(&mut self, global: &JSGlobalObject, val: JSValue) { let Some(v) = self.strong.get() else { return }; - let _ = JSPromise::opaque_mut(v.as_promise().unwrap()).resolve(global, val); + let _ = JSPromise::opaque_ref(v.as_promise().unwrap()).resolve(global, val); } pub fn reject( @@ -423,14 +423,14 @@ impl JSPromise { /// Fulfill an existing promise with the value. /// The value can be another Promise. /// If you want to create a new Promise that is already resolved, see `resolved_promise_value`. - pub fn resolve(&mut self, global: &JSGlobalObject, value: JSValue) -> Result<(), JsTerminated> { + pub fn resolve(&self, global: &JSGlobalObject, value: JSValue) -> Result<(), JsTerminated> { // `[[ZIG_EXPORT(check_slow)]]` crate::cpp::JSC__JSPromise__resolve(self, global, value) .map_err(|_| JsTerminated::JSTerminated) } pub fn reject( - &mut self, + &self, global: &JSGlobalObject, value: JsResult, ) -> Result<(), JsTerminated> { @@ -456,7 +456,7 @@ impl JSPromise { } pub fn reject_as_handled( - &mut self, + &self, global: &JSGlobalObject, value: JSValue, ) -> Result<(), JsTerminated> { @@ -470,7 +470,7 @@ impl JSPromise { /// of the event loop (threadpool callback) where the error would otherwise /// have an empty stack trace. pub fn reject_with_async_stack( - &mut self, + &self, global: &JSGlobalObject, value: JsResult, ) -> Result<(), JsTerminated> { @@ -496,7 +496,7 @@ impl JSPromise { self.to_js() } - pub fn unwrap(&mut self, vm: &VM, mode: UnwrapMode) -> Unwrapped { + pub fn unwrap(&self, vm: &VM, mode: UnwrapMode) -> Unwrapped { match self.status() { Status::Pending => Unwrapped::Pending, Status::Fulfilled => Unwrapped::Fulfilled(self.result(vm)), diff --git a/src/jsc/JSSecrets.rs b/src/jsc/JSSecrets.rs index 78369d55ef47..722288c26069 100644 --- a/src/jsc/JSSecrets.rs +++ b/src/jsc/JSSecrets.rs @@ -1,38 +1,71 @@ use crate::{AnyTaskJob, AnyTaskJobCtx, JSGlobalObject, JSValue, JsResult, Strong}; -// Opaque pointer to C++ SecretsJobOptions struct -bun_opaque::opaque_ffi! { pub struct SecretsJobOptions; } +/// The C++ object itself. Only the extern declarations below name this type; +/// all Rust code uses the owning [`SecretsJobOptions`] handle. +pub mod sys { + bun_opaque::opaque_ffi! { + /// C++ `SecretsJobOptions`. `&Self` is ABI-identical to a non-null + /// `SecretsJobOptions*` and carries no `noalias`/`readonly` — the + /// threadpool body writes `error`/`resultPassword`/`deleted` through it. + pub struct SecretsJobOptions; + } +} + +// C++ `SecretsJobOptions::fromJS` does a plain `new`, handing Rust the sole +// ownership unit. `deinit` is the matching `delete`; the dtor memsets the +// service/name/password buffers, so dropping is load-bearing, not just free. +bun_opaque::foreign_handle! { + /// Owned handle to a C++ `SecretsJobOptions`. + /// + /// Holds one heap allocation; `Drop` runs the C++ `delete`, which zeroes the + /// secret buffers. Every method takes `&self`: the ZST is `UnsafeCell`-backed, + /// so C++ mutates the job's result fields through `&` and there is no `&mut` + /// exclusivity to claim — the work pool and the C++ side share the object. + pub struct SecretsJobOptions(sys::SecretsJobOptions) via Bun__SecretsJobOptions__deinit; +} -// safe fn: `SecretsJobOptions` and `JSGlobalObject` are `opaque_ffi!` ZST -// handles (`!Freeze` via `UnsafeCell`); `&mut`/`&` are ABI-identical to -// non-null `*mut`/`*const` and C++ mutating job state through them is interior -// to the cell. `deinit` consumes/frees the C++ allocation and so stays -// `unsafe fn` (double-free precondition). +// safe fn: `sys::SecretsJobOptions` and `JSGlobalObject` are `opaque_ffi!` ZST +// handles (`!Freeze` via `UnsafeCell`), so `&T` is ABI-identical to a non-null +// pointer and C++ mutating through it is interior to the cell. unsafe extern "C" { - safe fn Bun__SecretsJobOptions__runTask(ctx: &mut SecretsJobOptions, global: &JSGlobalObject); + safe fn Bun__SecretsJobOptions__runTask(opts: &sys::SecretsJobOptions, global: &JSGlobalObject); safe fn Bun__SecretsJobOptions__runFromJS( - ctx: &mut SecretsJobOptions, + opts: &sys::SecretsJobOptions, global: &JSGlobalObject, promise: JSValue, ); - fn Bun__SecretsJobOptions__deinit(ctx: *mut SecretsJobOptions); + // safe: C++ `delete opts`. Freeing is not exclusive access, so the receiver + // is `&`. Reachable only via `ForeignRef`'s `Drop`, which owns the one unit + // it gives back — that pairing is the double-free proof. + safe fn Bun__SecretsJobOptions__deinit(opts: &sys::SecretsJobOptions); +} + +/// Job body. `&self` throughout: C++ writes the result fields through the same +/// pointer, and neither call takes or gives back an ownership unit. +impl SecretsJobOptions { + /// Runs OFF the JS thread; performs the platform keychain call. + pub fn run_task(&self, global: &JSGlobalObject) { + Bun__SecretsJobOptions__runTask(self.raw(), global) + } + + /// Runs ON the JS thread; settles `promise` from the job's result fields. + pub fn run_from_js(&self, global: &JSGlobalObject, promise: JSValue) { + Bun__SecretsJobOptions__runFromJS(self.raw(), global, promise) + } } +/// Owns the job options for the life of the task; both fields drop themselves, +/// `options` before `promise`, exactly where the old hand-rolled `Drop` ran. pub(crate) struct SecretsCtx { - ctx: *mut SecretsJobOptions, + options: SecretsJobOptions, promise: Strong, } impl AnyTaskJobCtx for SecretsCtx { fn run(&mut self, global: *mut JSGlobalObject) { - // `ctx` is a valid C++ SecretsJobOptions* held alive until Drop; - // `global` is the creating VM's global pointer. Both are `opaque_ffi!` - // ZST handles, so `opaque_mut`/`opaque_ref` are the centralised - // zero-byte deref proofs (panic on null). - Bun__SecretsJobOptions__runTask( - SecretsJobOptions::opaque_mut(self.ctx), - JSGlobalObject::opaque_ref(global), - ); + // `global` is the creating VM's global pointer, forwarded to C++ without + // being dereferenced here; `opaque_ref` is the zero-byte deref proof. + self.options.run_task(JSGlobalObject::opaque_ref(global)); } fn then(&mut self, global: &JSGlobalObject) -> JsResult<()> { @@ -46,32 +79,33 @@ impl AnyTaskJobCtx for SecretsCtx { // scope here, `drainMicrotasks`'s `TopExceptionScope` ctor asserts on the // unchecked simulated throw — same shape as `JSCDeferredWorkTask::run`. crate::validation_scope!(scope, global); - Bun__SecretsJobOptions__runFromJS(SecretsJobOptions::opaque_mut(self.ctx), global, promise); + self.options.run_from_js(global, promise); scope.assert_no_exception_except_termination() } } -impl Drop for SecretsCtx { - fn drop(&mut self) { - // SAFETY: `ctx` is the C++ SecretsJobOptions* passed to `create`; C++ side owns cleanup. - unsafe { Bun__SecretsJobOptions__deinit(self.ctx) }; - // `promise: Strong` drops automatically. - } -} - pub(crate) type SecretsJob = AnyTaskJob; -// Helper function for C++ to call with opaque pointer +/// `jsSecretsGet`/`Set`/`Delete` hand over a fresh `new SecretsJobOptions`; +/// Rust adopts it and is solely responsible for the `delete`. +/// +/// # Safety +/// `options` must be a live, uniquely-owned `SecretsJobOptions*`. #[unsafe(no_mangle)] -pub(crate) extern "C" fn Bun__Secrets__scheduleJob( +pub(crate) unsafe extern "C" fn Bun__Secrets__scheduleJob( global: &JSGlobalObject, - options: *mut SecretsJobOptions, + options: *mut sys::SecretsJobOptions, promise: JSValue, ) { + // SAFETY: caller contract. Non-null: every call site does + // `RETURN_IF_EXCEPTION` + `ASSERT(options)` after `fromJS`. + let options = unsafe { SecretsJobOptions::adopt_ptr(options) } + .expect("Bun__Secrets__scheduleJob: null SecretsJobOptions"); + // On `Err` the job is freed, running `SecretsCtx`'s drop. SecretsJob::create_and_schedule( global, SecretsCtx { - ctx: options, + options, promise: Strong::create(promise, global), }, ) diff --git a/src/jsc/JSUint8Array.rs b/src/jsc/JSUint8Array.rs index 7b1819bf1b18..dc2b61fbee4c 100644 --- a/src/jsc/JSUint8Array.rs +++ b/src/jsc/JSUint8Array.rs @@ -32,6 +32,9 @@ impl JSUint8Array { } } + /// `&mut self`, unlike the ZST-only methods above: the returned slice aliases + /// the typed array's real backing store, so the exclusive borrow is the only + /// thing preventing two live `&mut [u8]` over the same bytes. pub fn slice(&mut self) -> &mut [u8] { // Note: detached/empty JSUint8Array has ptr=null, len=0; // `ffi::slice_mut` tolerates `(null, 0)` so no extra guard. diff --git a/src/jsc/MarkedArgumentBuffer.rs b/src/jsc/MarkedArgumentBuffer.rs index e5e8bb4fc1ec..f9664f5e2ede 100644 --- a/src/jsc/MarkedArgumentBuffer.rs +++ b/src/jsc/MarkedArgumentBuffer.rs @@ -44,7 +44,7 @@ impl MarkedArgumentBuffer { ctx.r.unwrap() } - pub fn append(&mut self, value: JSValue) { + pub fn append(&self, value: JSValue) { MarkedArgumentBuffer__append(self, value) } diff --git a/src/jsc/RegularExpression.rs b/src/jsc/RegularExpression.rs index 9fe143252cd9..2cf2b6e74f85 100644 --- a/src/jsc/RegularExpression.rs +++ b/src/jsc/RegularExpression.rs @@ -1,8 +1,31 @@ +use core::mem::ManuallyDrop; +use core::ptr::NonNull; + use bun_core::String as BunString; -bun_opaque::opaque_ffi! { - /// Opaque FFI handle for `JSC::Yarr::RegularExpression`. - pub struct RegularExpression; +/// The C++ object itself. Only the extern declarations below name this type; +/// all Rust code uses the owning [`RegularExpression`] handle. +pub mod sys { + bun_opaque::opaque_ffi! { + /// `JSC::Yarr::RegularExpression`. `&Self` is ABI-identical to a non-null + /// `RegularExpression*`, and carries no `noalias`/`readonly` - C++ mutates + /// the match cursor through it. + pub struct RegularExpression; + } +} + +// C++ hands back a `new RegularExpression` (a `+1`); one `RegularExpression` +// handle owns exactly that allocation, and `deinit` (`delete re`) gives it back. +bun_opaque::foreign_handle! { + /// Owned handle to a C++ `JSC::Yarr::RegularExpression`. + /// + /// Owns one allocation; `Drop` deletes it. Every method takes `&self`: the ZST is + /// `UnsafeCell`-backed and C++ advances the match cursor through the same pointer, + /// so there is no `&mut self` to have. + /// + /// A handle borrowed from a pointer someone else owns (see [`Self::borrow_leaked`]) + /// is a `ManuallyDrop` - dropping it would free their regex. + pub struct RegularExpression(sys::RegularExpression) via Yarr__RegularExpression__deinit; } #[repr(u16)] @@ -28,71 +51,82 @@ pub enum RegularExpressionError { bun_core::named_error_set!(RegularExpressionError); -// `RegularExpression` is an opaque `UnsafeCell`-backed ZST handle, so -// `&RegularExpression` is ABI-identical to a non-null `*const` and C++ mutating -// internal Yarr state through it is interior mutation invisible to Rust. The -// query/compile shims are therefore declared `safe fn`; only `deinit` (which -// frees the allocation) keeps a raw `*mut` and stays `unsafe`. +// `&sys::RegularExpression` is ABI-identical to a non-null `RegularExpression*` +// and carries no `noalias`; Yarr mutates through it. Every shim traffics only in +// that plus POD, so all are `safe fn` — `deinit` releases, it is not exclusive. unsafe extern "C" { - safe fn Yarr__RegularExpression__init(pattern: BunString, flags: u16) - -> *mut RegularExpression; - fn Yarr__RegularExpression__deinit(pattern: *mut RegularExpression); - safe fn Yarr__RegularExpression__isValid(this: &RegularExpression) -> bool; - safe fn Yarr__RegularExpression__matchedLength(this: &RegularExpression) -> i32; + safe fn Yarr__RegularExpression__init( + pattern: BunString, + flags: u16, + ) -> *mut sys::RegularExpression; + safe fn Yarr__RegularExpression__deinit(this: &sys::RegularExpression); + safe fn Yarr__RegularExpression__isValid(this: &sys::RegularExpression) -> bool; + safe fn Yarr__RegularExpression__matchedLength(this: &sys::RegularExpression) -> i32; // C++: int Yarr__RegularExpression__searchRev(RegularExpression*, BunString) (bindings/RegularExpression.cpp:30) - safe fn Yarr__RegularExpression__searchRev(this: &RegularExpression, string: BunString) -> i32; - safe fn Yarr__RegularExpression__matches(this: &RegularExpression, string: BunString) -> i32; + safe fn Yarr__RegularExpression__searchRev( + this: &sys::RegularExpression, + string: BunString, + ) -> i32; + safe fn Yarr__RegularExpression__matches( + this: &sys::RegularExpression, + string: BunString, + ) -> i32; } +/// Construction and queries. `&self` throughout: C++ mutates the match cursor +/// through the same pointer. impl RegularExpression { + /// Borrow a regex handed to a foreign owner by [`Self::leak`]. + /// + /// Takes **no** ownership, hence `ManuallyDrop`: dropping this would free an + /// allocation the leak's new owner still holds. + /// + /// # Safety + /// `ptr` must be live for the returned handle's lifetime. #[inline] - pub fn init( - pattern: BunString, - flags: Flags, - ) -> Result<*mut RegularExpression, RegularExpressionError> { - let regex = Yarr__RegularExpression__init(pattern, flags as u16); - // `RegularExpression` is an `opaque_ffi!` ZST handle; `opaque_mut` is - // the centralised non-null-ZST deref proof (panics on null, which - // `Yarr__RegularExpression__init` never returns). - if !RegularExpression::opaque_mut(regex).is_valid() { - // SAFETY: `regex` is a valid live Yarr handle we just allocated; consumed here. - unsafe { Self::destroy(regex) }; + pub unsafe fn borrow_leaked(ptr: NonNull) -> ManuallyDrop { + // SAFETY: caller contract; ManuallyDrop never releases it. + ManuallyDrop::new(unsafe { Self::adopt(ptr) }) + } + + /// C++ `new`s the regex. On an invalid pattern the handle drops here, so the + /// allocation is freed before the `Err` reaches the caller. + #[inline] + pub fn init(pattern: BunString, flags: Flags) -> Result { + // SAFETY: C++ `init` transfers a fresh `+1` allocation (or null) to us. + let regex = + unsafe { Self::adopt_ptr(Yarr__RegularExpression__init(pattern, flags as u16)) } + .expect("Yarr__RegularExpression__init returned null"); + if !regex.is_valid() { return Err(RegularExpressionError::InvalidRegExp); } Ok(regex) } #[inline] - pub fn is_valid(&mut self) -> bool { - Yarr__RegularExpression__isValid(self) + pub fn is_valid(&self) -> bool { + Yarr__RegularExpression__isValid(self.raw()) } // Reserving `match` for a full match result. // #[inline] - // pub fn r#match(&mut self, str: BunString, start_from: i32) -> MatchResult { + // pub fn r#match(&self, str: BunString, start_from: i32) -> MatchResult { // } /// Simple boolean matcher #[inline] - pub fn matches(&mut self, str: BunString) -> bool { - Yarr__RegularExpression__matches(self, str) >= 0 - } - - #[inline] - pub fn search_rev(&mut self, str: BunString) -> i32 { - Yarr__RegularExpression__searchRev(self, str) + pub fn matches(&self, str: BunString) -> bool { + Yarr__RegularExpression__matches(self.raw(), str) >= 0 } #[inline] - pub fn matched_length(&mut self) -> i32 { - Yarr__RegularExpression__matchedLength(self) + pub fn search_rev(&self, str: BunString) -> i32 { + Yarr__RegularExpression__searchRev(self.raw(), str) } - /// Destroys the FFI-allocated handle. Caller must not use `this` afterwards. #[inline] - pub unsafe fn destroy(this: *mut Self) { - // SAFETY: `this` is a valid live Yarr RegularExpression handle; consumed here. - unsafe { Yarr__RegularExpression__deinit(this) } + pub fn matched_length(&self) -> i32 { + Yarr__RegularExpression__matchedLength(self.raw()) } } @@ -105,25 +139,24 @@ impl RegularExpression { // ────────────────────────────────────────────────────────────────────────── #[unsafe(no_mangle)] -pub(crate) fn __bun_regex_compile(pattern: BunString) -> Option> { +pub(crate) fn __bun_regex_compile(pattern: BunString) -> Option> { // Initialize JSC before first compile (idempotent). crate::initialize(false); - match RegularExpression::init(pattern, Flags::None) { - Ok(r) => core::ptr::NonNull::new(r.cast()), - Err(_) => None, - } + // The allocation is leaked to the caller, which owns it until `__bun_regex_drop`. + RegularExpression::init(pattern, Flags::None) + .ok() + .map(|r| r.leak().cast::<()>()) } #[unsafe(no_mangle)] -pub(crate) fn __bun_regex_matches(regex: core::ptr::NonNull<()>, input: &BunString) -> bool { - // `RegularExpression` is an `opaque_ffi!` ZST handle; `opaque_mut` is the - // centralised non-null deref proof. `regex` was produced by - // `__bun_regex_compile` and remains live until `__bun_regex_drop`. - RegularExpression::opaque_mut(regex.as_ptr().cast()).matches(*input) +pub(crate) fn __bun_regex_matches(regex: NonNull<()>, input: &BunString) -> bool { + // SAFETY: `regex` was leaked by `__bun_regex_compile` and stays live until + // `__bun_regex_drop`; the borrow releases nothing. + unsafe { RegularExpression::borrow_leaked(regex.cast()) }.matches(*input) } #[unsafe(no_mangle)] -pub(crate) fn __bun_regex_drop(regex: core::ptr::NonNull<()>) { - // SAFETY: `regex` was produced by `__bun_regex_compile`; consumed here. - unsafe { RegularExpression::destroy(regex.as_ptr().cast()) } +pub(crate) fn __bun_regex_drop(regex: NonNull<()>) { + // SAFETY: re-adopts the allocation leaked by `__bun_regex_compile`; `Drop` frees it. + drop(unsafe { RegularExpression::adopt(regex.cast()) }) } diff --git a/src/jsc/SourceProvider.rs b/src/jsc/SourceProvider.rs index 40ed2161c424..b716951f46d0 100644 --- a/src/jsc/SourceProvider.rs +++ b/src/jsc/SourceProvider.rs @@ -1,16 +1,32 @@ -bun_opaque::opaque_ffi! { - /// Opaque representation of a JavaScript source provider - pub struct SourceProvider; +/// The C++ object itself. Only the extern declaration below names this type; +/// all Rust code uses the owning [`SourceProvider`] handle. +pub mod sys { + bun_opaque::opaque_ffi! { + /// `JSC::SourceProvider`. `&Self` is ABI-identical to a non-null + /// `JSC::SourceProvider*`, and carries no `noalias`/`readonly` — C++ + /// mutates the intrusive refcount through it. + pub struct SourceProvider; + } } -impl SourceProvider { - pub fn deref(&mut self) { - JSC__SourceProvider__deref(self) - } +// C++ hands Rust a `+1` by `provider->ref()`-ing into the +// `ZigStackTrace::referenced_source_provider` out-param field +// (`populateStackFramePosition`, ZigException.cpp). One handle owns that ref. +bun_opaque::foreign_handle! { + /// Owned handle to a C++ `JSC::SourceProvider` (a `WTF::RefCounted`). + /// + /// Holds one ref on the intrusive refcount; `Drop` gives it back. There is no + /// `&mut self` API and no `DerefMut`: a refcount is shared by definition, and + /// JSC mutates the provider through the same pointer. + /// + /// `Option` niche-optimizes to a single thin pointer, so it is + /// exactly the ABI of the C++ `JSC::SourceProvider*` struct field. + pub struct SourceProvider(sys::SourceProvider) via JSC__SourceProvider__deref; } unsafe extern "C" { - // safe: `SourceProvider` is an opaque `UnsafeCell`-backed ZST handle; `&mut` is - // ABI-identical to a non-null pointer and C++ refcount mutation is interior. - safe fn JSC__SourceProvider__deref(provider: &mut SourceProvider); + // safe: C++ takes `JSC::SourceProvider*` and calls the intrusive `->deref()`. + // A refcount decrement is not exclusive access — other refs exist by + // definition — so the receiver is `&`, not `&mut`. + safe fn JSC__SourceProvider__deref(provider: &sys::SourceProvider); } diff --git a/src/jsc/Strong.rs b/src/jsc/Strong.rs index 18118a53983a..2aa46912e0da 100644 --- a/src/jsc/Strong.rs +++ b/src/jsc/Strong.rs @@ -6,24 +6,83 @@ use core::ptr::NonNull; use crate::{JSGlobalObject, JSValue}; -// Note: field renamed from `impl` (Rust keyword) to `handle`. -pub struct Strong { - handle: NonNull, - // NonNull is already !Send + !Sync, matching the requirement that - // Strong must be dropped on the JS thread (HandleSet is VM-owned). +/// The C++ allocation itself. Only the extern declarations below name this +/// type; all Rust code uses the owning [`Impl`] handle. +pub mod sys { + bun_opaque::opaque_ffi! { + /// A `JSC::HandleSet` slot (`JSC::JSValue*`); see StrongRef.cpp. `&Self` + /// is ABI-identical to a non-null slot pointer and carries no + /// `noalias`/`readonly` — JSC writes the slot through it. + pub struct Impl; + } +} + +/// A corrupted slot pointer segfaults inside JSC's `HandleBlock::handleSet`, +/// which loses the Rust caller frame; panicking here names the exact owner. +/// `0x10000` is Windows' null-page guard — real slots are bmalloc'd far above. +fn strong_ref_delete(slot: &sys::Impl) { + if cfg!(debug_assertions) { + assert!( + (std::ptr::from_ref(slot) as usize) >= 0x10000, + "Strong::drop: corrupted HandleSlot pointer {slot:p}" + ); + } + Bun__StrongRef__delete(slot) } +// `Bun__StrongRef__new` allocates a HandleSlot from the VM's HandleSet and +// hands back its sole owner. One `Impl` handle owns exactly that one slot. +bun_opaque::foreign_handle! { + /// Owned handle to one `JSC::HandleSet` slot rooting a `JSValue`. + /// + /// `Drop` deallocates the slot. Every method takes `&self`: JSC writes the slot + /// through the same pointer, and deallocating it is not exclusive access. + pub struct Impl(sys::Impl) via strong_ref_delete; +} + +/// Slot lifecycle and access. `&self` throughout: JSC mutates the slot. +impl Impl { + /// C++ allocates the slot and hands back its sole owner. + pub fn init(global: &JSGlobalObject, value: JSValue) -> Self { + crate::mark_binding!(); + let p = NonNull::new(Bun__StrongRef__new(global, value)) + .expect("Bun__StrongRef__new returned null"); + // SAFETY: freshly allocated slot, owned by nobody else. + unsafe { Self::adopt(p) } + } + + pub fn get(&self) -> JSValue { + // The slot *is* a `JSC::JSValue`; see StrongRef.cpp. + // SAFETY: the slot is a live, aligned JSC::JSValue for `self`'s + // lifetime; `DecodedJSValue` is its `#[repr(C)]` ABI-compatible mirror. + unsafe { (*self.as_ptr().cast::()).encode() } + } + + pub fn set(&self, global: &JSGlobalObject, value: JSValue) { + crate::mark_binding!(); + Bun__StrongRef__set(self.raw(), global, value); + } + + pub fn clear(&self) { + crate::mark_binding!(); + Bun__StrongRef__clear(self.raw()); + } +} + +// `ForeignRef` is !Send + !Sync, matching the requirement that Strong must be +// dropped on the JS thread (HandleSet is VM-owned). +#[repr(transparent)] +pub struct Strong(Impl); + impl Strong { /// Hold a strong reference to a JavaScript value. Released on `Drop`. pub fn create(value: JSValue, global: &JSGlobalObject) -> Strong { debug_assert!(!value.is_empty()); - Strong { - handle: Impl::init(global, value), - } + Strong(Impl::init(global, value)) } pub fn get(&self) -> JSValue { - let result = Impl::get(self.handle); + let result = self.0.get(); debug_assert!(!result.is_empty()); result } @@ -31,67 +90,56 @@ impl Strong { /// Set a new value for the strong reference. pub fn set(&mut self, global: &JSGlobalObject, new_value: JSValue) { debug_assert!(!new_value.is_empty()); - Impl::set(self.handle, global, new_value); + self.0.set(global, new_value); } /// Swap a new value for the strong reference. pub fn swap(&mut self, global: &JSGlobalObject, new_value: JSValue) -> JSValue { - let result = Impl::get(self.handle); + let result = self.0.get(); self.set(global, new_value); result } - /// Adopt an `Impl` handle allocated externally (e.g. by C++ bindgen glue), - /// taking ownership. The handle will be destroyed on `Drop`. + /// Adopt a slot allocated externally (e.g. by C++ bindgen glue), taking + /// ownership. The slot is deallocated on `Drop`. /// /// # Safety /// `handle` must have been produced by `Bun__StrongRef__new` (or equivalent) /// and must not be owned by any other `Strong`/`Optional`. - pub unsafe fn adopt(handle: NonNull) -> Strong { - Strong { handle } - } -} - -impl Drop for Strong { - /// Release the strong reference. - fn drop(&mut self) { - // SAFETY: `self.handle` came from `Impl::init` and is consumed exactly once here. - unsafe { Impl::destroy(self.handle) }; + pub unsafe fn adopt(handle: NonNull) -> Strong { + // SAFETY: caller transfers the allocation. + Strong(unsafe { Impl::adopt(handle) }) } } /// Holds a strong reference to a JS value, protecting it from garbage /// collection. When not holding a value, the strong may still be allocated. -// Note: field renamed from `impl` (Rust keyword) to `handle`. -// `#[repr(transparent)]` over a single nullable pointer keeps this FFI-safe -// when embedded in `extern "C"` structs. +// `#[repr(transparent)]` over a niche-optimized `Option` (one nullable +// pointer) keeps this FFI-safe when embedded in `extern "C"` structs. #[repr(transparent)] #[derive(Default)] -pub struct Optional { - handle: Option>, -} +pub struct Optional(Option); impl Optional { pub const fn empty() -> Optional { - Optional { handle: None } + Optional(None) } - /// Adopt an `Impl` handle allocated externally (e.g. by C++ bindgen glue), - /// taking ownership if non-null. The handle will be destroyed on `Drop`. + /// Adopt a slot allocated externally (e.g. by C++ bindgen glue), taking + /// ownership if non-null. The slot is deallocated on `Drop`. /// /// # Safety /// If `Some`, `handle` must have been produced by `Bun__StrongRef__new` /// (or equivalent) and must not be owned by any other `Strong`/`Optional`. - pub unsafe fn adopt(handle: Option>) -> Optional { - Optional { handle } + pub unsafe fn adopt(handle: Option>) -> Optional { + // SAFETY: caller transfers the allocation, if any. + Optional(handle.map(|p| unsafe { Impl::adopt(p) })) } /// Hold a strong reference to a JavaScript value. Released on `Drop` or `clear`. pub fn create(value: JSValue, global: &JSGlobalObject) -> Optional { if !value.is_empty() { - Optional { - handle: Some(Impl::init(global, value)), - } + Optional(Some(Impl::init(global, value))) } else { Optional::empty() } @@ -99,8 +147,8 @@ impl Optional { /// Clears the value, but does not de-allocate the Strong reference. pub fn clear_without_deallocation(&mut self) { - let Some(r) = self.handle else { return }; - Impl::clear(r); + let Some(r) = &self.0 else { return }; + r.clear(); } pub fn call(&mut self, global: &JSGlobalObject, args: &[JSValue]) -> JSValue { @@ -113,8 +161,7 @@ impl Optional { } pub fn get(&self) -> Option { - let imp = self.handle?; - let result = Impl::get(imp); + let result = self.0.as_ref()?.get(); if result.is_empty() { return None; } @@ -122,20 +169,20 @@ impl Optional { } pub fn swap(&mut self) -> JSValue { - let Some(imp) = self.handle else { + let Some(imp) = &self.0 else { return JSValue::ZERO; }; - let result = Impl::get(imp); + let result = imp.get(); if result.is_empty() { return JSValue::ZERO; } - Impl::clear(imp); + imp.clear(); result } pub fn has(&self) -> bool { - let Some(r) = self.handle else { return false }; - !Impl::get(r).is_empty() + let Some(r) = &self.0 else { return false }; + !r.get().is_empty() } pub fn try_swap(&mut self) -> Option { @@ -146,97 +193,31 @@ impl Optional { Some(result) } - /// Explicit teardown. Idempotent; equivalent to dropping in place and - /// leaving `self` empty so `Drop` is a no-op. + /// Explicit teardown. Idempotent; leaves `self` empty. pub fn deinit(&mut self) { - let Some(r) = self.handle.take() else { return }; - // SAFETY: `r` came from `Impl::init` and is consumed exactly once here. - unsafe { Impl::destroy(r) }; + drop(self.0.take()); } pub fn set(&mut self, global: &JSGlobalObject, value: JSValue) { - let Some(r) = self.handle else { - if value.is_empty() { - return; - } - self.handle = Some(Impl::init(global, value)); - return; - }; - Impl::set(r, global, value); - } -} - -impl Drop for Optional { - /// Frees memory for the underlying Strong reference. - fn drop(&mut self) { - let Some(r) = self.handle.take() else { return }; - // SAFETY: `r` came from `Impl::init` and is consumed exactly once here. - unsafe { Impl::destroy(r) }; - } -} - -bun_opaque::opaque_ffi! { - /// Opaque FFI handle. Backed by a `JSC::JSValue`-sized HandleSlot; see Strong.cpp. - pub struct Impl; -} - -impl Impl { - pub fn init(global: &JSGlobalObject, value: JSValue) -> NonNull { - crate::mark_binding!(); - NonNull::new(Bun__StrongRef__new(global, value)).expect("Bun__StrongRef__new returned null") - } - - pub fn get(this: NonNull) -> JSValue { - // `this` is actually a pointer to a `JSC::JSValue`; see Strong.cpp. - // SAFETY: HandleSlot storage is a live, aligned JSC::JSValue for the - // lifetime of the Impl handle; `DecodedJSValue` is its `#[repr(C)]` - // ABI-compatible mirror. - unsafe { (*this.as_ptr().cast::()).encode() } - } - - pub fn set(this: NonNull, global: &JSGlobalObject, value: JSValue) { - crate::mark_binding!(); - Bun__StrongRef__set(Impl::opaque_ref(this.as_ptr()), global, value); - } - - pub fn clear(this: NonNull) { - crate::mark_binding!(); - Bun__StrongRef__clear(Impl::opaque_ref(this.as_ptr())); - } - - /// SAFETY: `this` must be a valid handle from `init`; consumed here (do not reuse). - pub unsafe fn destroy(this: NonNull) { - crate::mark_binding!(); - // Defensive: a corrupted slot pointer here segfaults inside JSC's - // HandleBlock::handleSet (the backing block is recovered by masking - // the slot to the block base, then `+0x10` is read), which loses the - // Rust caller frame. With panic=abort the crash-handler hook captures - // a Rust backtrace, so a `panic!` at this layer surfaces the *exact* - // call site that holds the corrupted Strong. The 0x10000 floor is - // Windows' default null-page guard; legitimate `Impl*` are bmalloc'd - // far above it. - if cfg!(debug_assertions) { - assert!( - (this.as_ptr() as usize) >= 0x10000, - "Strong* corrupted ({:p}); owning struct was overwritten", - this.as_ptr(), - ); + if let Some(r) = &self.0 { + r.set(global, value); + } else if !value.is_empty() { + self.0 = Some(Impl::init(global, value)); } - // SAFETY: caller contract guarantees `this` is a live handle from - // `Bun__StrongRef__new`; ownership is transferred to C++ which frees it. - unsafe { Bun__StrongRef__delete(this.as_ptr()) }; } } -// `Impl` and `JSGlobalObject` are opaque `UnsafeCell`-backed ZST handles, so -// `&Impl`/`&JSGlobalObject` are ABI-identical to non-null `*const T` and C++ -// mutating through them (HandleSet slot write) is interior mutation invisible -// to Rust. `delete` consumes the C++ allocation and so stays `unsafe fn`. +// `sys::Impl` and `JSGlobalObject` are opaque `UnsafeCell`-backed ZST handles, +// so `&T` is ABI-identical to a non-null `*const T` and the HandleSet slot +// write C++ performs through them is interior mutation invisible to Rust. unsafe extern "C" { - fn Bun__StrongRef__delete(this: *mut Impl); - safe fn Bun__StrongRef__new(global: &JSGlobalObject, value: JSValue) -> *mut Impl; - safe fn Bun__StrongRef__set(this: &Impl, global: &JSGlobalObject, value: JSValue); - safe fn Bun__StrongRef__clear(this: &Impl); + // safe: C++ hands the slot to `HandleSet::deallocate`. Deallocating is not + // exclusive access — the slot is JSC's, not Rust's — so the receiver is + // `&`, not `&mut`. `foreign_owned!` requires a `safe fn` here. + safe fn Bun__StrongRef__delete(this: &sys::Impl); + safe fn Bun__StrongRef__new(global: &JSGlobalObject, value: JSValue) -> *mut sys::Impl; + safe fn Bun__StrongRef__set(this: &sys::Impl, global: &JSGlobalObject, value: JSValue); + safe fn Bun__StrongRef__clear(this: &sys::Impl); } pub use crate::deprecated_strong as deprecated; diff --git a/src/jsc/TextCodec.rs b/src/jsc/TextCodec.rs index 29688c5507cb..2b705f602dac 100644 --- a/src/jsc/TextCodec.rs +++ b/src/jsc/TextCodec.rs @@ -3,23 +3,49 @@ use core::ptr::NonNull; use crate::mark_binding; use bun_core::String as BunString; +/// The C++ object itself. Only the extern declarations below name this type; +/// all Rust code uses the owning [`TextCodec`] handle. +pub mod sys { + bun_opaque::opaque_ffi! { + /// `PAL::TextCodec`. `&Self` is ABI-identical to a non-null + /// `PAL::TextCodec*` (C++ spells it `void*`), and carries no + /// `noalias`/`readonly` — C++ mutates the codec's streaming state + /// (lead byte, ISO-2022-JP mode, GB18030 bytes) through it. + pub struct TextCodec; + } +} + +// C++ `newTextCodec(encoding).release()` hands back the sole owning pointer; +// `Bun__deleteTextCodec` `delete`s it. One handle owns exactly one codec. +bun_opaque::foreign_handle! { + /// Owned handle to a C++ `PAL::TextCodec`. + /// + /// `Drop` deletes the codec. Every method takes `&self`: C++ mutates the codec + /// through the same pointer, and giving the object back is not exclusive + /// access, so there is no `&mut self` and no `DerefMut`. + pub struct TextCodec(sys::TextCodec) via Bun__deleteTextCodec; +} + +// `&sys::TextCodec` is ABI-identical to the `void*` the C++ shims declare. Shims +// that also take raw `*const u8` / out-pointers stay `unsafe fn`: safe Rust can +// forge those. unsafe extern "C" { fn Bun__createTextCodec( encoding_name: *const u8, encoding_name_len: usize, - ) -> Option>; + ) -> *mut sys::TextCodec; fn Bun__decodeWithTextCodec( - codec: *mut TextCodec, + codec: &sys::TextCodec, data: *const u8, length: usize, flush: bool, stop_on_error: bool, out_saw_error: *mut bool, ) -> BunString; - fn Bun__deleteTextCodec(codec: *mut TextCodec); - // safe: `TextCodec` is an `opaque_ffi!` ZST handle; `&mut` is ABI-identical - // to a non-null `*mut` and C++ mutating codec state is interior to the cell. - safe fn Bun__stripBOMFromTextCodec(codec: &mut TextCodec); + // safe: C++ `delete`s the codec. Handing the object back is not exclusive + // access as Rust sees it, so the receiver is `&`, not `&mut`. + safe fn Bun__deleteTextCodec(codec: &sys::TextCodec); + safe fn Bun__stripBOMFromTextCodec(codec: &sys::TextCodec); fn Bun__isEncodingSupported(encoding_name: *const u8, encoding_name_len: usize) -> bool; fn Bun__getCanonicalEncodingName( encoding_name: *const u8, @@ -28,39 +54,29 @@ unsafe extern "C" { ) -> Option>; } -bun_opaque::opaque_ffi! { - /// Opaque FFI handle to a C++ PAL::TextCodec. - pub struct TextCodec; -} - pub struct DecodeResult { pub result: BunString, pub saw_error: bool, } impl TextCodec { - pub fn create(encoding: &[u8]) -> Option> { - mark_binding!(); - // SAFETY: encoding.ptr is valid for encoding.len bytes. - unsafe { Bun__createTextCodec(encoding.as_ptr(), encoding.len()) } - } - - // FFI-owned opaque; constructed/destroyed across FFI, so explicit - // destroy instead of `impl Drop` (cannot own a `TextCodec` by value). - pub unsafe fn destroy(this: *mut TextCodec) { + /// `None` when `encoding` does not name a valid WebKit encoding. + pub fn create(encoding: &[u8]) -> Option { mark_binding!(); - // SAFETY: caller guarantees `this` was returned by `create` and not yet freed. - unsafe { Bun__deleteTextCodec(this) } + // SAFETY: encoding.ptr is valid for encoding.len bytes; C++ + // `newTextCodec(encoding).release()` transfers us the sole owning + // pointer, or null. + unsafe { Self::adopt_ptr(Bun__createTextCodec(encoding.as_ptr(), encoding.len())) } } - pub fn decode(&mut self, data: &[u8], flush: bool, stop_on_error: bool) -> DecodeResult { + pub fn decode(&self, data: &[u8], flush: bool, stop_on_error: bool) -> DecodeResult { mark_binding!(); let mut saw_error: bool = false; - // SAFETY: `self` is a valid live codec; `data` valid for `data.len()` bytes; - // `saw_error` is a valid out-pointer for the duration of the call. + // SAFETY: `data` valid for `data.len()` bytes; `saw_error` is a valid + // out-pointer for the duration of the call. let result = unsafe { Bun__decodeWithTextCodec( - self, + self.raw(), data.as_ptr(), data.len(), flush, @@ -72,9 +88,9 @@ impl TextCodec { DecodeResult { result, saw_error } } - pub fn strip_bom(&mut self) { + pub fn strip_bom(&self) { mark_binding!(); - Bun__stripBOMFromTextCodec(self) + Bun__stripBOMFromTextCodec(self.raw()) } pub fn is_supported(encoding: &[u8]) -> bool { diff --git a/src/jsc/URL.rs b/src/jsc/URL.rs index bc2fe0fa2cb2..c5af6720deda 100644 --- a/src/jsc/URL.rs +++ b/src/jsc/URL.rs @@ -1,48 +1,63 @@ -use core::ptr::NonNull; - use bun_core::String; use bun_jsc::{JSGlobalObject, JSValue, JsResult}; -bun_opaque::opaque_ffi! { - /// Opaque handle to a WebKit `WTF::URL` allocated on the C++ side. - pub struct URL; +/// The C++ object itself. Only the extern declarations below name this type; +/// all Rust code uses the owning [`URL`] handle. +pub mod sys { + bun_opaque::opaque_ffi! { + /// A heap-allocated WebKit `WTF::URL`. `&Self` is ABI-identical to a + /// non-null `WTF::URL*` and carries no `noalias`/`readonly`. + pub struct URL; + } } -// Getters take `&URL` (non-null `*const URL` at the C ABI; BunString.cpp never -// mutates the WTF::URL on read). `&mut String` for the in/out params is -// ABI-identical to non-null `*mut String`. `URL__deinit` consumes the C++ -// allocation, so it keeps a raw pointer and stays `unsafe fn`. +// C++ allocates (`new WTF::URL(...)`) and hands the allocation to Rust; +// `URL__deinit` is an unconditional `delete`. One `URL` handle owns exactly +// that one allocation. +bun_opaque::foreign_handle! { + /// Owned handle to a C++ heap `WTF::URL`. + /// + /// `Drop` `delete`s the allocation. Every method takes `&self`: the C++ side + /// never mutates the `WTF::URL` on read, and destroying it is not exclusive + /// access in Rust's sense. + pub struct URL(sys::URL) via URL__deinit; +} + +// Getters take `&sys::URL` (a non-null `WTF::URL*` at the C ABI; BunString.cpp +// never mutates it on read); `&mut String` is ABI-identical to `*mut String`. +// Every shim traffics only in those plus value types, so all are `safe fn`. unsafe extern "C" { - safe fn URL__fromJS(value: JSValue, global: &JSGlobalObject) -> *mut URL; - safe fn URL__fromString(input: &mut String) -> *mut URL; - safe fn URL__protocol(url: &URL) -> String; - safe fn URL__href(url: &URL) -> String; - safe fn URL__username(url: &URL) -> String; - safe fn URL__password(url: &URL) -> String; - safe fn URL__search(url: &URL) -> String; - safe fn URL__host(url: &URL) -> String; - safe fn URL__hostname(url: &URL) -> String; - safe fn URL__port(url: &URL) -> u32; - fn URL__deinit(url: *mut URL); - safe fn URL__pathname(url: &URL) -> String; + safe fn URL__fromJS(value: JSValue, global: &JSGlobalObject) -> *mut sys::URL; + safe fn URL__fromString(input: &mut String) -> *mut sys::URL; + safe fn URL__protocol(url: &sys::URL) -> String; + safe fn URL__href(url: &sys::URL) -> String; + safe fn URL__username(url: &sys::URL) -> String; + safe fn URL__password(url: &sys::URL) -> String; + safe fn URL__search(url: &sys::URL) -> String; + safe fn URL__host(url: &sys::URL) -> String; + safe fn URL__hostname(url: &sys::URL) -> String; + safe fn URL__port(url: &sys::URL) -> u32; + // safe: C++ `delete`s the `WTF::URL*`. Reached only through `Drop`. + safe fn URL__deinit(url: &sys::URL); + safe fn URL__pathname(url: &sys::URL) -> String; safe fn URL__getHrefFromJS(value: JSValue, global: &JSGlobalObject) -> String; safe fn URL__getHref(input: &mut String) -> String; safe fn URL__getFileURLString(input: &mut String) -> String; safe fn URL__getHrefJoin(base: &mut String, relative: &mut String) -> String; safe fn URL__pathFromFileURL(input: &mut String) -> String; - safe fn URL__hash(url: &URL) -> String; - safe fn URL__fragmentIdentifier(url: &URL) -> String; + safe fn URL__hash(url: &sys::URL) -> String; + safe fn URL__fragmentIdentifier(url: &sys::URL) -> String; } impl URL { /// Includes the leading '#'. pub fn hash(&self) -> String { - URL__hash(self) + URL__hash(self.raw()) } /// Exactly the same as hash, excluding the leading '#'. pub fn fragment_identifier(&self) -> String { - URL__fragmentIdentifier(self) + URL__fragmentIdentifier(self.raw()) } pub fn href_from_string(str: String) -> String { @@ -73,40 +88,42 @@ impl URL { crate::call_check_slow(global, || URL__getHrefFromJS(value, global)) } + /// C++ `new WTF::URL` on success; `None` if the URL is invalid. #[track_caller] - pub fn from_js(value: JSValue, global: &JSGlobalObject) -> JsResult>> { - crate::call_check_slow(global, || URL__fromJS(value, global)).map(NonNull::new) + pub fn from_js(value: JSValue, global: &JSGlobalObject) -> JsResult> { + // SAFETY: `URL__fromJS` transfers a fresh `new WTF::URL` (or null) to us. + crate::call_check_slow(global, || URL__fromJS(value, global)) + .map(|p| unsafe { Self::adopt_ptr(p) }) } - pub fn from_utf8(input: &[u8]) -> Option> { + pub fn from_utf8(input: &[u8]) -> Option { Self::from_string(String::borrow_utf8(input)) } - pub fn from_string(str: String) -> Option> { + pub fn from_string(str: String) -> Option { let mut input = str; - NonNull::new(URL__fromString(&mut input)) + // SAFETY: `URL__fromString` transfers a fresh `new WTF::URL` (or null) to us. + unsafe { Self::adopt_ptr(URL__fromString(&mut input)) } } - // from_js/from_string/from_utf8 return an owned C++ heap pointer that the - // caller must destroy(). pub fn protocol(&self) -> String { - URL__protocol(self) + URL__protocol(self.raw()) } pub fn href(&self) -> String { - URL__href(self) + URL__href(self.raw()) } pub fn username(&self) -> String { - URL__username(self) + URL__username(self.raw()) } pub fn password(&self) -> String { - URL__password(self) + URL__password(self.raw()) } pub fn search(&self) -> String { - URL__search(self) + URL__search(self.raw()) } /// Returns the host WITHOUT the port. @@ -118,7 +135,7 @@ impl URL { /// URL("http://example.com:8080").host() => "example.com" /// ``` pub fn host(&self) -> String { - URL__host(self) + URL__host(self.raw()) } /// Returns the host WITH the port. @@ -130,23 +147,16 @@ impl URL { /// URL("http://example.com:8080").hostname() => "example.com:8080" /// ``` pub fn hostname(&self) -> String { - URL__hostname(self) + URL__hostname(self.raw()) } /// Returns `u32::MAX` if the port is not set. Otherwise, `port` /// is guaranteed to be within the `u16` range. pub fn port(&self) -> u32 { - URL__port(self) - } - - // Kept as explicit destroy (not Drop) — URL is an opaque #[repr(C)] FFI - // handle constructed/destroyed across the C++ boundary. - pub unsafe fn destroy(this: *mut Self) { - // SAFETY: `this` is a valid *URL from C++; freed exactly once - unsafe { URL__deinit(this) } + URL__port(self.raw()) } pub fn pathname(&self) -> String { - URL__pathname(self) + URL__pathname(self.raw()) } } diff --git a/src/jsc/URLSearchParams.rs b/src/jsc/URLSearchParams.rs index e911c3324684..c0bcba4e32a0 100644 --- a/src/jsc/URLSearchParams.rs +++ b/src/jsc/URLSearchParams.rs @@ -12,11 +12,11 @@ bun_opaque::opaque_ffi! { unsafe extern "C" { safe fn URLSearchParams__create(global_object: &JSGlobalObject, init: &ZigString) -> JSValue; safe fn URLSearchParams__fromJS(value: JSValue) -> Option>; - // safe: `URLSearchParams` is an `opaque_ffi!` ZST handle (`&mut` is - // ABI-identical to a non-null `*mut`); `ctx` is an opaque round-trip pointer - // C++ only forwards to `callback` (synchronous, never retained). + // safe: `URLSearchParams` is an `opaque_ffi!` ZST handle (`&` is ABI-identical + // to a non-null pointer and carries no `noalias`/`readonly`); `ctx` is an opaque + // round-trip pointer C++ only forwards to `callback` (synchronous, never retained). safe fn URLSearchParams__toString( - self_: &mut URLSearchParams, + self_: &URLSearchParams, ctx: *mut c_void, callback: extern "C" fn(ctx: *mut c_void, str: *const ZigString), ); @@ -33,7 +33,7 @@ impl URLSearchParams { URLSearchParams__fromJS(value) } - pub fn to_string(&mut self, ctx: &mut Ctx, callback: fn(ctx: &mut Ctx, str: ZigString)) { + pub fn to_string(&self, ctx: &mut Ctx, callback: fn(ctx: &mut Ctx, str: ZigString)) { // A fn pointer cannot be a const generic, so pack (ctx, callback) on the // stack and pass the pair through the C trampoline's void* context. struct Wrap<'a, Ctx> { diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index 225a27c91fd7..7456fef05085 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -5784,7 +5784,7 @@ impl VirtualMachine { }); let code: Option<&[u8]> = if is_error_instance { // SAFETY: `is_error_instance` ⇒ `get_object()` is `Some`. - let obj = unsafe { &mut *error_instance.get_object().unwrap_unchecked() }; + let obj = unsafe { &*error_instance.get_object().unwrap_unchecked() }; if let Some(code_value) = obj.get_code_property_vm_inquiry(global_ref) { if code_value.is_string() { match code_value.to_bun_string(global_ref) { diff --git a/src/jsc/Weak.rs b/src/jsc/Weak.rs index 7cd505ec7e27..c5bbea4a2f35 100644 --- a/src/jsc/Weak.rs +++ b/src/jsc/Weak.rs @@ -12,72 +12,81 @@ pub enum WeakRefType { PostgreSQLQueryClient = 2, } -bun_opaque::opaque_ffi! { - /// Opaque FFI handle (C++ `Bun::WeakRef`). - pub(crate) struct WeakImpl; +/// The C++ object itself. Only the extern declarations below name this type; +/// all Rust code uses the owning [`WeakImpl`] handle. +pub(crate) mod sys { + bun_opaque::opaque_ffi! { + /// `Bun::WeakRef`. `&Self` is ABI-identical to a non-null + /// `Bun::WeakRef*`, and carries no `noalias`/`readonly` — C++ mutates + /// the `JSC::Weak` slot through it. + pub(crate) struct WeakImpl; + } +} + +// C++ `new Bun::WeakRef` hands back the allocation. One `WeakImpl` handle owns +// exactly that one allocation; `Drop` gives it back. +bun_opaque::foreign_handle! { + /// Owned handle to a C++ `Bun::WeakRef`. + /// + /// Every method takes `&self`: C++ mutates the `JSC::Weak` slot through the + /// same pointer, so there is no `&mut self` to have. + pub(crate) struct WeakImpl(sys::WeakImpl) via Bun__WeakRef__delete; +} + +// `JSGlobalObject`/`sys::WeakImpl` are ZST handles: `&T` is ABI-identical to a +// non-null `*const T`, and C++ writing the slot through it is interior mutation. +// Shims trafficking only in such refs + scalars are `safe fn`. +unsafe extern "C" { + // safe: C++ clears the slot and `delete`s the object. Destruction is not + // exclusive access of any Rust-visible bytes, so the receiver is `&`. + safe fn Bun__WeakRef__delete(this: &sys::WeakImpl); + // NOT `safe fn`: C++ stores `ctx` in the `JSC::Weak` and hands it to + // `Bun___finalize`, which dereferences it. Safe Rust can forge a + // `*mut c_void`, so the call itself carries the validity obligation. + fn Bun__WeakRef__new( + global: &JSGlobalObject, + value: JSValue, + ref_type: WeakRefType, + ctx: *mut c_void, + ) -> *mut sys::WeakImpl; + safe fn Bun__WeakRef__get(this: &sys::WeakImpl) -> JSValue; + safe fn Bun__WeakRef__clear(this: &sys::WeakImpl); } impl WeakImpl { - pub(crate) fn init( + fn new( global_this: &JSGlobalObject, value: JSValue, ref_type: WeakRefType, ctx: Option>, - ) -> NonNull { - NonNull::new(Bun__WeakRef__new( - global_this, - value, - ref_type, - ctx.map_or(core::ptr::null_mut(), |p| p.as_ptr()), - )) - .expect("Bun__WeakRef__new returned null") + ) -> Self { + // SAFETY: C++ only stores `ctx` and forwards it to the type's finalizer; + // the owner it points at outlives this handle. + let ptr = unsafe { + Bun__WeakRef__new( + global_this, + value, + ref_type, + ctx.map_or(core::ptr::null_mut(), |p| p.as_ptr()), + ) + }; + // SAFETY: `Bun__WeakRef__new` transfers a fresh allocation, or null. + unsafe { Self::adopt_ptr(ptr) }.expect("Bun__WeakRef__new returned null") } /// Read the weakly-held `JSValue` (or `JSValue::ZERO` if collected). - /// - /// Safe: every `NonNull` in this crate originates from - /// [`WeakImpl::init`] and is held by a [`Weak`] that drops it via - /// [`WeakImpl::destroy`] before releasing the slot — so any - /// `NonNull` reachable here is a live C++ `JSC::Weak` handle. - /// Same contract as [`crate::strong::Impl::get`]. - pub(crate) fn get(this: NonNull) -> JSValue { - Bun__WeakRef__get(WeakImpl::opaque_ref(this.as_ptr())) + fn get(&self) -> JSValue { + Bun__WeakRef__get(self.raw()) } - /// Clear the weakly-held value without freeing the handle. - /// - /// Safe for the same reason as [`WeakImpl::get`] — the handle is live by - /// construction; `clear` is idempotent and does not invalidate `this`. - pub(crate) fn clear(this: NonNull) { - Bun__WeakRef__clear(WeakImpl::opaque_ref(this.as_ptr())) + /// Clear the weakly-held value without freeing the handle; idempotent. + fn clear(&self) { + Bun__WeakRef__clear(self.raw()) } - - pub(crate) unsafe fn destroy(this: NonNull) { - // SAFETY: `this` is a live WeakImpl handle; consumed here. - unsafe { Bun__WeakRef__delete(this.as_ptr()) } - } -} - -// `WeakImpl` is an opaque `UnsafeCell`-backed ZST handle (`&WeakImpl` is -// ABI-identical to non-null `*const WeakImpl`; C++ slot mutation is interior). -// `new` is `safe fn`: `&JSGlobalObject` is the non-null handle proof, and `ctx` -// is an opaque round-trip pointer C++ only stores and forwards to the finalizer -// (never dereferenced as Rust data) — same contract as `JSC__VM__holdAPILock`. -// `delete` consumes the allocation and so stays `unsafe fn`. -unsafe extern "C" { - fn Bun__WeakRef__delete(this: *mut WeakImpl); - safe fn Bun__WeakRef__new( - global: &JSGlobalObject, - value: JSValue, - ref_type: WeakRefType, - ctx: *mut c_void, - ) -> *mut WeakImpl; - safe fn Bun__WeakRef__get(this: &WeakImpl) -> JSValue; - safe fn Bun__WeakRef__clear(this: &WeakImpl); } pub struct Weak { - r#ref: Option>, + r#ref: Option, global_this: Option, _ctx: PhantomData<*mut T>, } @@ -114,7 +123,7 @@ impl Weak { ) -> Self { if !value.is_empty() { return Self { - r#ref: Some(WeakImpl::init( + r#ref: Some(WeakImpl::new( global_this, value, ref_type, @@ -133,8 +142,7 @@ impl Weak { } pub fn get(&self) -> Option { - let r#ref = self.r#ref?; - let result = WeakImpl::get(r#ref); + let result = self.r#ref.as_ref()?.get(); if result.is_empty() { return None; } @@ -143,23 +151,23 @@ impl Weak { } pub fn swap(&mut self) -> JSValue { - let Some(r#ref) = self.r#ref else { + let Some(r#ref) = self.r#ref.as_ref() else { return JSValue::ZERO; }; - let result = WeakImpl::get(r#ref); + let result = r#ref.get(); if result.is_empty() { return JSValue::ZERO; } - WeakImpl::clear(r#ref); + r#ref.clear(); result } pub fn has(&self) -> bool { - let Some(r#ref) = self.r#ref else { + let Some(r#ref) = self.r#ref.as_ref() else { return false; }; - !WeakImpl::get(r#ref).is_empty() + !r#ref.get().is_empty() } pub fn try_swap(&mut self) -> Option { @@ -172,20 +180,9 @@ impl Weak { } pub fn clear(&mut self) { - let Some(r#ref) = self.r#ref else { - return; - }; - WeakImpl::clear(r#ref); - } -} - -impl Drop for Weak { - fn drop(&mut self) { - let Some(r#ref) = self.r#ref else { + let Some(r#ref) = self.r#ref.as_ref() else { return; }; - self.r#ref = None; - // SAFETY: `r#ref` was live; we just took ownership and are deleting it. - unsafe { WeakImpl::destroy(r#ref) }; + r#ref.clear(); } } diff --git a/src/jsc/ZigException.rs b/src/jsc/ZigException.rs index 123cdac745e7..94651c917ea7 100644 --- a/src/jsc/ZigException.rs +++ b/src/jsc/ZigException.rs @@ -72,11 +72,9 @@ impl ZigException { frame.deinit(); } - if let Some(source) = self.stack.referenced_source_provider { - // Pointer was set by JSC (C++) and is valid until this deref releases it. - // `SourceProvider` is an opaque ZST handle. - crate::SourceProvider::opaque_mut(source.as_ptr()).deref(); - } + // Gives back the `+1` C++ took in `populateStackFramePosition`. `take()` + // nulls the slot, so the `Holder::drop` re-entry path releases nothing. + drop(self.stack.referenced_source_provider.take()); } // `ZigException__fromException` is declared in headers.h but has no C++ diff --git a/src/jsc/ZigStackTrace.rs b/src/jsc/ZigStackTrace.rs index de55243069b2..834a0427ccf9 100644 --- a/src/jsc/ZigStackTrace.rs +++ b/src/jsc/ZigStackTrace.rs @@ -1,5 +1,4 @@ use core::ptr; -use core::ptr::NonNull; use crate::schema_api as api; use bun_core::String as BunString; @@ -21,12 +20,12 @@ pub struct ZigStackTrace { pub frames_len: u8, pub frames_cap: u8, - /// Non-null if `source_lines_*` points into data owned by a JSC::SourceProvider. - /// If so, then .deref must be called on it to release the memory. + /// `Some` if `source_lines_*` points into data owned by a `JSC::SourceProvider`. + /// C++ `ref()`s the provider before storing it here; `Drop` gives that ref back. /// - /// `Option>` niche-optimizes to a single thin pointer, so the - /// FFI layout is exactly one nullable pointer. - pub referenced_source_provider: Option>, + /// `Option` niche-optimizes to a single thin pointer, so the + /// FFI layout is exactly one nullable `JSC::SourceProvider*`. + pub referenced_source_provider: Option, } impl ZigStackTrace { diff --git a/src/jsc/array_buffer.rs b/src/jsc/array_buffer.rs index 01a8e12a08af..a932170514cd 100644 --- a/src/jsc/array_buffer.rs +++ b/src/jsc/array_buffer.rs @@ -127,7 +127,7 @@ unsafe extern "C" { ptr: *const c_void, len: usize, ) -> JSValue; - fn JSC__ArrayBuffer__asBunArrayBuffer(self_: *mut JSCArrayBuffer, out: *mut ArrayBuffer); + fn JSC__ArrayBuffer__asBunArrayBuffer(self_: &JSCArrayBuffer, out: *mut ArrayBuffer); // safe: `JSCArrayBuffer` is an `opaque_ffi!` ZST handle (`!Freeze` via // `UnsafeCell`); `&` is ABI-identical to a non-null `*mut` and the C++ // `RefCounted` count mutation is interior to the opaque cell. @@ -1153,7 +1153,7 @@ unsafe impl bun_ptr::ExternalSharedDescriptor for JSCArrayBuffer { } impl JSCArrayBuffer { - pub fn as_array_buffer(&mut self) -> ArrayBuffer { + pub fn as_array_buffer(&self) -> ArrayBuffer { let mut out = core::mem::MaybeUninit::::uninit(); // SAFETY: C++ fully initializes `out`. unsafe { diff --git a/src/jsc/bindgen.rs b/src/jsc/bindgen.rs index 58f87065b632..a528d5e4378d 100644 --- a/src/jsc/bindgen.rs +++ b/src/jsc/bindgen.rs @@ -76,9 +76,10 @@ pub struct BindgenStrongAny; impl Bindgen for BindgenStrongAny { type ZigType = Strong; - // `?*jsc.Strong.Impl` — must be single-word for #[repr(C)] union placement, so - // `Option>` (niche-optimized), NOT `Option<*mut T>` (two words). - type ExternType = Option>; + // `?*jsc.Strong.Impl` — the C++ HandleSlot, not the owning Rust handle. Must + // be single-word for #[repr(C)] union placement, so `Option>` + // (niche-optimized), NOT `Option<*mut T>` (two words). + type ExternType = Option>; fn convert_from_extern(extern_value: Self::ExternType) -> Self::ZigType { // SAFETY: bindgen contract — C++ passes a freshly-allocated Strong handle diff --git a/src/jsc/event_loop.rs b/src/jsc/event_loop.rs index 9b421243378f..f26e45651f07 100644 --- a/src/jsc/event_loop.rs +++ b/src/jsc/event_loop.rs @@ -676,9 +676,6 @@ impl EventLoop { /// Without this, the last worker's close-task lambda — and the /// `WebWorker` box reachable through its `protectedThis` — leak. pub fn drop_concurrent_cpp_tasks(&mut self) { - unsafe extern "C" { - fn Bun__deleteEventLoopTask(task: *mut CppTask); - } let mut iter = self.concurrent_tasks.pop_batch().iterator(); loop { let node = iter.next(); @@ -690,10 +687,16 @@ impl EventLoop { // freeing here is sound. let (task, auto_delete) = unsafe { ((*node).task, (*node).auto_delete()) }; if task.tag == bun_event_loop::task_tag::CppTask { - // SAFETY: every `CppTask` payload is a heap + // SAFETY: every `CppTask` payload is a non-null heap // `WebCore::EventLoopTask*` (`ScriptExecutionContext::postTask*` - // → `new EventLoopTask`); we own it once popped. - unsafe { Bun__deleteEventLoopTask(task.ptr.cast::()) }; + // → `new EventLoopTask`); we own it once popped, and `Drop` + // deletes it without running. + let task = unsafe { + CppTask::adopt(NonNull::new_unchecked( + task.ptr.cast::(), + )) + }; + drop(task); } else { // Hand non-Cpp payloads to `self.tasks` so `deinit()`'s // existing per-tag reclaim handles them. diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index 240f02153fa3..9bab910756bd 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -1223,8 +1223,8 @@ pub use self::js_promise::Strong as JSPromiseStrong; pub use self::js_promise::Status as PromiseStatus; /// `bun_ptr::RefPtr` — intrusive refcounted smart pointer. Re-exported here so -/// `crate::RefPtr` (ZigStackTrace.rs) resolves without every -/// submodule taking a direct `bun_ptr` dep. +/// submodules can write `crate::RefPtr` without each taking a direct +/// `bun_ptr` dep. pub use bun_ptr::RefPtr; /// `bun.String` — refcounted WTF-backed string. Re-exported at the crate root diff --git a/src/jsc/rare_data.rs b/src/jsc/rare_data.rs index 93e35d544254..ff1523a84a51 100644 --- a/src/jsc/rare_data.rs +++ b/src/jsc/rare_data.rs @@ -197,7 +197,9 @@ impl CleanupHook { // ────────────────────────────────────────────────────────────────────────── pub struct RareData { - pub boring_ssl_engine: Option<*mut boring::ENGINE>, + /// Owning handle to the VM's lazily-created BoringSSL `ENGINE`; field `Drop` + /// calls `ENGINE_free`. Consumers borrow it via [`RareData::boring_engine`]. + pub boring_ssl_engine: Option, /// Erased `*mut webcore::blob::Store` (intrusive-refcounted on the runtime /// side). Constructed via `__bun_stdio_blob_store_new`; high tier casts back. @@ -667,14 +669,20 @@ impl RareData { .get_or_insert_with(|| Box::new(FilePollStore::init())) } - pub fn boring_engine(&mut self) -> *mut boring::ENGINE { - // The raw `ENGINE_new()` result is cached without a null check: - // `EVP_DigestInit_ex` tolerates a NULL engine, so OOM here degrades to - // "no engine" rather than crashing. Debug-assert to surface it without - // altering release behavior. - let ptr = *self + /// Borrows the VM-owned `ENGINE` as a raw pointer. Every consumer + /// (`EVP_DigestInit_ex`, `EVP_Digest`, `HMAC_Init_ex`, `SHA256::hash`) only + /// reads it; the `boring_ssl_engine` handle stays the owner. + pub fn boring_engine(&mut self) -> *mut boring::sys::ENGINE { + // A failed `ENGINE_new()` is not cached: `EVP_DigestInit_ex` tolerates a + // NULL engine, so OOM here degrades to "no engine" rather than crashing. + // Debug-assert to surface it without altering release behavior. + if self.boring_ssl_engine.is_none() { + self.boring_ssl_engine = boring::ENGINE::new(); + } + let ptr = self .boring_ssl_engine - .get_or_insert_with(|| boring::ENGINE_new()); + .as_ref() + .map_or(core::ptr::null_mut(), |engine| engine.as_ptr()); debug_assert!(!ptr.is_null(), "ENGINE_new returned null"); ptr } @@ -1048,18 +1056,15 @@ impl Drop for RareData { fn drop(&mut self) { // temp_pipe_read_buffer / spawn_sync_event_loop_ / s3_default_client / // default_csrf_secret / cleanup_hooks / cron_jobs / path_buf / - // tls_default_ciphers: + // tls_default_ciphers / boring_ssl_engine (an owning `ENGINE` handle, + // whose Drop calls ENGINE_free): // all dropped automatically via field Drop. - if let Some(engine) = self.boring_ssl_engine.take() { - // SAFETY: engine was created by ENGINE_new. - unsafe { boring::ENGINE_free(engine) }; - } debug_assert!(self.cron_jobs.is_empty()); if let Some(s) = self.default_client_ssl_ctx.take() { - // SAFETY: returned by ssl_ctx_cache.get_or_create_opts with +1 ref. - unsafe { boring::SSL_CTX_free(s) }; + // Returned by ssl_ctx_cache.get_or_create_opts with a +1 ref. + boring::SSL_CTX_free(SslCtx::opaque_ref(s)); } // After the default-ctx free so the tombstone callback still finds a live // map; ssl_ctx_cache itself lives in `RuntimeState` and is dropped there. diff --git a/src/jsc/virtual_machine_exports.rs b/src/jsc/virtual_machine_exports.rs index cacc80863c1d..fb8460cfddab 100644 --- a/src/jsc/virtual_machine_exports.rs +++ b/src/jsc/virtual_machine_exports.rs @@ -100,8 +100,10 @@ pub fn ensure_process_ipc_initialized(global: &JSGlobalObject) { /// This function is called on the main thread /// The bunVM() call will assert this // HOST_EXPORT(Bun__queueTask, c) -pub fn queue_task(global: &JSGlobalObject, task: *mut crate::cpp_task::CppTask) { +pub fn queue_task(global: &JSGlobalObject, task: *mut crate::cpp_task::sys::CppTask) { crate::mark_binding!(); + // C++ hands over the sole owner; the queue holds it until `CppTask::run` + // or `EventLoop::drop_concurrent_cpp_tasks` gives it back. global .bun_vm() .event_loop_mut() @@ -125,8 +127,9 @@ pub fn report_unhandled_error(global: &JSGlobalObject, value: JSValue) -> JSValu /// The main difference: we need to allocate the task & wakeup the thread /// We can avoid that if we run it from the main thread. // HOST_EXPORT(Bun__queueTaskConcurrently, c) -pub fn queue_task_concurrently(global: &JSGlobalObject, task: *mut crate::cpp_task::CppTask) { +pub fn queue_task_concurrently(global: &JSGlobalObject, task: *mut crate::cpp_task::sys::CppTask) { crate::mark_binding!(); + // C++ hands over the sole owner; the concurrent queue holds it. // SAFETY: bun_vm_concurrently() yields the live VM; `event_loop()` never // returns null for a Bun-owned global. Called off-thread but the loop // wakeup is thread-safe. diff --git a/src/libarchive/lib.rs b/src/libarchive/lib.rs index 2477005a3af7..7b9ae7087349 100644 --- a/src/libarchive/lib.rs +++ b/src/libarchive/lib.rs @@ -32,14 +32,20 @@ pub mod lib { pub type la_int64_t = i64; type time_t = isize; + /// The C object itself. Only the extern declarations below name this type; + /// owning Rust code uses the [`Archive`] handle. + pub mod sys { + bun_opaque::opaque_ffi! { + /// libarchive `struct archive`. `&Self` is ABI-identical to a non-null + /// `struct archive*` and carries no `noalias`/`readonly` — libarchive + /// mutates through every call. + pub struct Archive; + } + } + bun_opaque::opaque_ffi! { - /// Opaque libarchive `struct archive`. Always used behind `*mut Archive`. - /// Contains `UnsafeCell` so that `&Archive` does not assert immutability - /// (libarchive mutates through every call), making `&self -> *mut Self` - /// sound under Stacked Borrows. - pub struct Archive; /// Opaque libarchive `struct archive_entry`. Always used behind `*mut Entry`. - /// Contains `UnsafeCell` for the same reason as `Archive` — the C side + /// Contains `UnsafeCell` for the same reason as `sys::Archive` — the C side /// mutates through getter/setter calls that take `&self` here. pub struct Entry; } @@ -70,28 +76,32 @@ pub mod lib { // so it is ABI-compatible with the C `int` return values. unsafe extern "C" { // read side - fn archive_read_new() -> *mut Archive; - fn archive_read_close(a: *mut Archive) -> Result; - fn archive_read_free(a: *mut Archive) -> Result; - fn archive_read_support_format_tar(a: *mut Archive) -> Result; - fn archive_read_support_format_gnutar(a: *mut Archive) -> Result; - fn archive_read_support_filter_gzip(a: *mut Archive) -> Result; - fn archive_read_set_options(a: *mut Archive, opts: *const c_char) -> Result; - fn archive_read_open_memory(a: *mut Archive, buf: *const c_void, size: usize) -> Result; - fn archive_read_next_header(a: *mut Archive, entry: *mut *mut Entry) -> Result; - fn archive_read_data(a: *mut Archive, buf: *mut c_void, size: usize) -> la_ssize_t; + safe fn archive_read_new() -> *mut sys::Archive; + safe fn archive_read_close(a: &sys::Archive) -> Result; + // safe: `archive_read_free` is `archive_free()` — vtable dispatch on + // `a->vtable->archive_free`, the one release for any `struct archive`. + // Freeing is not exclusive access, so the receiver is `&`, not `&mut`. + safe fn archive_read_free(a: &sys::Archive) -> Result; + safe fn archive_read_support_format_tar(a: &sys::Archive) -> Result; + safe fn archive_read_support_format_gnutar(a: &sys::Archive) -> Result; + safe fn archive_read_support_filter_gzip(a: &sys::Archive) -> Result; + fn archive_read_set_options(a: &sys::Archive, opts: *const c_char) -> Result; + fn archive_read_open_memory(a: &sys::Archive, buf: *const c_void, size: usize) -> Result; + fn archive_read_next_header(a: &sys::Archive, entry: *mut *mut Entry) -> Result; + fn archive_read_data(a: &sys::Archive, buf: *mut c_void, size: usize) -> la_ssize_t; fn archive_read_data_block( - a: *mut Archive, + a: &sys::Archive, buff: *mut *const c_void, size: *mut usize, offset: *mut la_int64_t, ) -> Result; - fn archive_error_string(a: *mut Archive) -> *const c_char; + safe fn archive_error_string(a: &sys::Archive) -> *const c_char; // streaming-read setup (used by TarballStream's resumable extractor) - pub fn archive_read_set_format(a: *mut Archive, code: c_int) -> c_int; - pub fn archive_read_append_filter(a: *mut Archive, code: c_int) -> c_int; + pub safe fn archive_read_set_format(a: &sys::Archive, code: c_int) -> c_int; + pub safe fn archive_read_append_filter(a: &sys::Archive, code: c_int) -> c_int; + // NOT `safe fn`: the registered callbacks dereference `client_data`. pub fn archive_read_open( - a: *mut Archive, + a: &sys::Archive, client_data: *mut c_void, open: Option, read: Option, @@ -99,25 +109,25 @@ pub mod lib { ) -> c_int; // write side - fn archive_write_new() -> *mut Archive; - fn archive_write_free(a: *mut Archive) -> Result; - fn archive_write_close(a: *mut Archive) -> Result; - fn archive_write_set_format_pax_restricted(a: *mut Archive) -> Result; - fn archive_write_add_filter_gzip(a: *mut Archive) -> Result; + safe fn archive_write_new() -> *mut sys::Archive; + safe fn archive_write_free(a: &sys::Archive) -> Result; + safe fn archive_write_close(a: &sys::Archive) -> Result; + safe fn archive_write_set_format_pax_restricted(a: &sys::Archive) -> Result; + safe fn archive_write_add_filter_gzip(a: &sys::Archive) -> Result; fn archive_write_set_filter_option( - a: *mut Archive, + a: &sys::Archive, module: *const c_char, option: *const c_char, value: *const c_char, ) -> Result; - fn archive_write_set_options(a: *mut Archive, opts: *const c_char) -> Result; - fn archive_write_open_filename(a: *mut Archive, filename: *const c_char) -> Result; - fn archive_write_header(a: *mut Archive, entry: *mut Entry) -> Result; - fn archive_write_data(a: *mut Archive, data: *const c_void, size: usize) -> la_ssize_t; - fn archive_write_finish_entry(a: *mut Archive) -> Result; + fn archive_write_set_options(a: &sys::Archive, opts: *const c_char) -> Result; + fn archive_write_open_filename(a: &sys::Archive, filename: *const c_char) -> Result; + fn archive_write_header(a: &sys::Archive, entry: *mut Entry) -> Result; + fn archive_write_data(a: &sys::Archive, data: *const c_void, size: usize) -> la_ssize_t; + safe fn archive_write_finish_entry(a: &sys::Archive) -> Result; #[link_name = "archive_write_open2"] fn archive_write_open2_raw( - a: *mut Archive, + a: &sys::Archive, client_data: *mut c_void, open: Option, write: Option, @@ -127,7 +137,7 @@ pub mod lib { // entry fn archive_entry_new() -> *mut Entry; - fn archive_entry_new2(a: *mut Archive) -> *mut Entry; + fn archive_entry_new2(a: &sys::Archive) -> *mut Entry; fn archive_entry_free(e: *mut Entry); fn archive_entry_clear(e: *mut Entry) -> *mut Entry; fn archive_entry_pathname(e: *mut Entry) -> *const c_char; @@ -146,6 +156,41 @@ pub mod lib { fn archive_entry_set_mtime(e: *mut Entry, secs: time_t, nsecs: c_long); } + /// `ForeignOwned::release` returns `()`; libarchive's status is not actionable + /// once the allocation is gone. + fn archive_free_release(a: &sys::Archive) { + let _ = archive_read_free(a); + } + + // libarchive allocates in `archive_read_new()` and hands back the object. + // One `Archive` handle owns exactly that one allocation. + bun_opaque::foreign_handle! { + /// Owned handle to a libarchive `struct archive` from `archive_read_new()`. + /// + /// `Drop` gives the allocation back. Every method reached through this handle + /// takes `&self`: libarchive mutates the archive through the same pointer, so + /// there is no `&mut self` to have and no `DerefMut`. + pub struct Archive(sys::Archive) via archive_free_release; + } + + impl Archive { + /// `archive_read_new()` — libarchive allocates; this handle owns it. + #[inline] + pub fn read_new() -> Self { + // SAFETY: `archive_read_new` hands back the sole ownership unit. + unsafe { Self::adopt_ptr(sys::Archive::read_new()) } + .expect("archive_read_new returned NULL (OOM)") + } + } + + impl core::ops::Deref for Archive { + type Target = sys::Archive; + #[inline] + fn deref(&self) -> &sys::Archive { + self.raw() + } + } + /// One block from `archive_read_data_block`. `bytes` borrows libarchive's /// internal buffer (valid until the next read call on the owning archive). pub struct Block<'a> { @@ -154,65 +199,56 @@ pub mod lib { pub result: Result, } - impl Archive { - pub fn read_new() -> *mut Archive { - // SAFETY: FFI call with no preconditions. - let p = unsafe { archive_read_new() }; + impl sys::Archive { + /// Raw `archive_read_new()`. Prefer [`Archive::read_new`]; this exists for + /// the callers that keep a `*mut sys::Archive` and free it by hand. + pub fn read_new() -> *mut sys::Archive { + let p = archive_read_new(); // libarchive's `archive_read_new()` returns NULL on calloc failure. - // Every caller immediately dereferences the result (forming - // `&Archive`), so fail loudly here instead of invoking UB at the - // first accessor call. + // Every caller immediately dereferences the result, so fail loudly + // here instead of invoking UB at the first accessor call. assert!(!p.is_null(), "archive_read_new returned NULL (OOM)"); p } pub fn read_close(&self) -> Result { - // SAFETY: self came from archive_read_new(). - unsafe { archive_read_close(self.as_mut_ptr()) } + archive_read_close(self) } pub fn read_free(&self) -> Result { - // SAFETY: self came from archive_read_new(); not used after this. - unsafe { archive_read_free(self.as_mut_ptr()) } + archive_read_free(self) } pub fn read_support_format_tar(&self) -> Result { - // SAFETY: self valid. - unsafe { archive_read_support_format_tar(self.as_mut_ptr()) } + archive_read_support_format_tar(self) } pub fn read_support_format_gnutar(&self) -> Result { - // SAFETY: self valid. - unsafe { archive_read_support_format_gnutar(self.as_mut_ptr()) } + archive_read_support_format_gnutar(self) } pub fn read_support_filter_gzip(&self) -> Result { - // SAFETY: self valid. - unsafe { archive_read_support_filter_gzip(self.as_mut_ptr()) } + archive_read_support_filter_gzip(self) } pub fn read_set_options(&self, opts: &core::ffi::CStr) -> Result { - // SAFETY: self valid; opts is NUL-terminated. - unsafe { archive_read_set_options(self.as_mut_ptr(), opts.as_ptr()) } + // SAFETY: opts is NUL-terminated. + unsafe { archive_read_set_options(self, opts.as_ptr()) } } pub fn read_open_memory(&self, buf: &[u8]) -> Result { - // SAFETY: self valid; buf outlives the archive (caller contract, - // see `BufferReadStream::buf` field comment). - unsafe { archive_read_open_memory(self.as_mut_ptr(), buf.as_ptr().cast(), buf.len()) } + // SAFETY: buf outlives the archive (caller contract, see + // `BufferReadStream::buf` field comment). + unsafe { archive_read_open_memory(self, buf.as_ptr().cast(), buf.len()) } } pub fn read_next_header(&self, entry: &mut *mut Entry) -> Result { - // SAFETY: self valid; entry is a valid out-ptr. - unsafe { - archive_read_next_header(self.as_mut_ptr(), std::ptr::from_mut::<*mut Entry>(entry)) - } + // SAFETY: entry is a valid out-ptr. + unsafe { archive_read_next_header(self, std::ptr::from_mut::<*mut Entry>(entry)) } } pub fn read_data(&self, buf: &mut [u8]) -> isize { - // SAFETY: self valid; buf writable for buf.len(). - unsafe { archive_read_data(self.as_mut_ptr(), buf.as_mut_ptr().cast(), buf.len()) } + // SAFETY: buf writable for buf.len(). + unsafe { archive_read_data(self, buf.as_mut_ptr().cast(), buf.len()) } } /// `archive_read_data_block` — returns `None` on EOF. pub fn next(&self, offset: &mut i64) -> Option> { let mut buff: *const c_void = core::ptr::null(); let mut size: usize = 0; - // SAFETY: self valid; out-ptrs are valid stack locations. - let r = unsafe { - archive_read_data_block(self.as_mut_ptr(), &raw mut buff, &raw mut size, offset) - }; + // SAFETY: out-ptrs are valid stack locations. + let r = unsafe { archive_read_data_block(self, &raw mut buff, &raw mut size, offset) }; if r == Result::Eof { return None; } @@ -346,12 +382,8 @@ pub mod lib { Result::Ok } - // `self` must be a live archive handle from `archive_{read,write}_new()`. - // `Archive` is `opaque_ffi!`-backed (UnsafeCell), so `&self → *mut Self` - // is sound; libarchive never returns null from `*_new()`. pub fn error_string(&self) -> &'static [u8] { - // SAFETY: `self` is a live archive handle. - let p = unsafe { archive_error_string(self.as_mut_ptr()) }; + let p = archive_error_string(self); if p.is_null() { return b""; } @@ -363,25 +395,20 @@ pub mod lib { } // ── write side ───────────────────────────────────────────────────── - pub fn write_new() -> *mut Archive { - // SAFETY: FFI call with no preconditions. - unsafe { archive_write_new() } + pub fn write_new() -> *mut sys::Archive { + archive_write_new() } pub fn write_free(&self) -> Result { - // SAFETY: self came from archive_write_new(); not used after this. - unsafe { archive_write_free(self.as_mut_ptr()) } + archive_write_free(self) } pub fn write_close(&self) -> Result { - // SAFETY: self valid. - unsafe { archive_write_close(self.as_mut_ptr()) } + archive_write_close(self) } pub fn write_set_format_pax_restricted(&self) -> Result { - // SAFETY: self valid. - unsafe { archive_write_set_format_pax_restricted(self.as_mut_ptr()) } + archive_write_set_format_pax_restricted(self) } pub fn write_add_filter_gzip(&self) -> Result { - // SAFETY: self valid. - unsafe { archive_write_add_filter_gzip(self.as_mut_ptr()) } + archive_write_add_filter_gzip(self) } pub fn write_set_filter_option( &self, @@ -389,10 +416,10 @@ pub mod lib { option: &ZStr, value: &ZStr, ) -> Result { - // SAFETY: self valid; ZStr guarantees NUL-termination. + // SAFETY: ZStr guarantees NUL-termination. unsafe { archive_write_set_filter_option( - self.as_mut_ptr(), + self, module.map_or(core::ptr::null(), |m| m.as_ptr().cast()), option.as_ptr().cast(), value.as_ptr().cast(), @@ -400,25 +427,24 @@ pub mod lib { } } pub fn write_set_options(&self, opts: &ZStr) -> Result { - // SAFETY: self valid; ZStr guarantees NUL-termination. - unsafe { archive_write_set_options(self.as_mut_ptr(), opts.as_ptr().cast()) } + // SAFETY: ZStr guarantees NUL-termination. + unsafe { archive_write_set_options(self, opts.as_ptr().cast()) } } pub fn write_open_filename(&self, filename: &ZStr) -> Result { - // SAFETY: self valid; ZStr guarantees NUL-termination. - unsafe { archive_write_open_filename(self.as_mut_ptr(), filename.as_ptr().cast()) } + // SAFETY: ZStr guarantees NUL-termination. + unsafe { archive_write_open_filename(self, filename.as_ptr().cast()) } } pub fn write_header(&self, entry: &Entry) -> Result { - // SAFETY: self valid; entry came from Entry::new()/read_next_header(). + // SAFETY: entry came from Entry::new()/read_next_header(). // `Entry` has interior mutability so `&Entry -> *mut Entry` is sound. - unsafe { archive_write_header(self.as_mut_ptr(), entry.as_mut_ptr()) } + unsafe { archive_write_header(self, entry.as_mut_ptr()) } } pub fn write_data(&self, data: &[u8]) -> isize { - // SAFETY: self valid; data readable for data.len(). - unsafe { archive_write_data(self.as_mut_ptr(), data.as_ptr().cast(), data.len()) } + // SAFETY: data readable for data.len(). + unsafe { archive_write_data(self, data.as_ptr().cast(), data.len()) } } pub fn write_finish_entry(&self) -> Result { - // SAFETY: self valid. - unsafe { archive_write_finish_entry(self.as_mut_ptr()) } + archive_write_finish_entry(self) } } @@ -462,9 +488,9 @@ pub mod lib { /// `archive_entry_new2(archive)` — ties the entry to the archive's /// charset-conversion context (preferred over `new()` when an archive /// is available). `archive` is a live handle from `read_new()`/`write_new()`. - pub fn new2(archive: &Archive) -> *mut Entry { - // SAFETY: `archive` is a live handle (opaque_ffi! `&self → *mut Self`). - unsafe { archive_entry_new2(archive.as_mut_ptr()) } + pub fn new2(archive: &sys::Archive) -> *mut Entry { + // SAFETY: `archive` is a live handle. + unsafe { archive_entry_new2(archive) } } pub fn free(&self) { // SAFETY: self came from Entry::new(); not used after this. @@ -502,110 +528,65 @@ pub mod lib { } } - // ── RAII owners ──────────────────────────────────────────────────────── - // - // The raw `*mut Archive` / `*mut Entry` constructors above mirror the C - // API. These thin owners pair them with the matching `*_free` on `Drop` - // so callers stop hand-rolling `defer { (*archive).read_free() }`. - - /// Owns a `*mut Archive` opened with [`Archive::read_new`]; calls - /// `archive_read_free` on drop. Derefs to `&Archive`. - pub struct ReadArchive(core::ptr::NonNull); - impl ReadArchive { - #[inline] - pub fn new() -> Self { - Self( - core::ptr::NonNull::new(Archive::read_new()) - .expect("archive_read_new returned null"), - ) - } - #[inline] - pub fn as_ptr(&self) -> *mut Archive { - self.0.as_ptr() - } - } - impl core::ops::Deref for ReadArchive { - type Target = Archive; - #[inline] - fn deref(&self) -> &Archive { - // SAFETY: handle is live until Drop; libarchive owns the storage. - unsafe { self.0.as_ref() } - } - } - impl Drop for ReadArchive { - #[inline] - fn drop(&mut self) { - // SAFETY: handle came from archive_read_new() and is freed exactly once. - let _ = unsafe { archive_read_free(self.0.as_ptr()) }; - } + // `sys::Archive` has two ownership disciplines: `archive_read_free`, claimed by + // its `ForeignOwned` impl and used by `Archive`, and `archive_write_free`, which + // names a marker. Both are the same `archive_free()` vtable entry — the pairing + // with `archive_write_new()` is what the type documents. + fn archive_write_free_release(a: &sys::Archive) { + let _ = archive_write_free(a); } + bun_opaque::foreign_release!(pub WriteFree => sys::Archive, archive_write_free_release); - /// Owns a `*mut Archive` opened with [`Archive::write_new`]; calls - /// `archive_write_free` on drop. Derefs to `&Archive`. - pub struct WriteArchive(core::ptr::NonNull); + bun_opaque::foreign_handle! { + /// Owns an archive opened with [`sys::Archive::write_new`]; `Drop` calls + /// `archive_write_free`. Derefs to `&sys::Archive`. + pub struct WriteArchive(sys::Archive) via marker WriteFree; + } impl WriteArchive { #[inline] pub fn new() -> Self { - Self( - core::ptr::NonNull::new(Archive::write_new()) - .expect("archive_write_new returned null"), - ) - } - #[inline] - pub fn as_ptr(&self) -> *mut Archive { - self.0.as_ptr() + // SAFETY: `archive_write_new` hands back the sole ownership unit. + unsafe { Self::adopt_ptr(sys::Archive::write_new()) } + .expect("archive_write_new returned null") } } impl core::ops::Deref for WriteArchive { - type Target = Archive; + type Target = sys::Archive; #[inline] - fn deref(&self) -> &Archive { - // SAFETY: handle is live until Drop; libarchive owns the storage. - unsafe { self.0.as_ref() } + fn deref(&self) -> &sys::Archive { + self.raw() } } - impl Drop for WriteArchive { - #[inline] - fn drop(&mut self) { - // SAFETY: handle came from archive_write_new() and is freed exactly once. - let _ = unsafe { archive_write_free(self.0.as_ptr()) }; - } + + fn entry_free_release(e: &Entry) { + // SAFETY: `e` is live and carries the unit `Drop` is giving back. + unsafe { archive_entry_free(e.as_mut_ptr()) }; } - /// Owns a `*mut Entry` created with [`Entry::new`] / [`Entry::new2`]; - /// calls `archive_entry_free` on drop. Derefs to `&Entry`. - pub struct OwnedEntry(core::ptr::NonNull); + bun_opaque::foreign_handle! { + /// Owns an entry created with [`Entry::new`] / [`Entry::new2`]; `Drop` calls + /// `archive_entry_free`. Derefs to `&Entry`. + pub struct OwnedEntry(Entry) via entry_free_release; + } impl OwnedEntry { #[inline] pub fn new() -> Self { - Self(core::ptr::NonNull::new(Entry::new()).expect("archive_entry_new returned null")) + // SAFETY: `archive_entry_new` hands back the sole ownership unit. + unsafe { Self::adopt_ptr(Entry::new()) }.expect("archive_entry_new returned null") } /// `archive` is a live handle from `read_new()`/`write_new()`. #[inline] pub fn new2(archive: &Archive) -> Self { - Self( - core::ptr::NonNull::new(Entry::new2(archive)) - .expect("archive_entry_new2 returned null"), - ) - } - #[inline] - pub fn as_ptr(&self) -> *mut Entry { - self.0.as_ptr() + // SAFETY: `archive_entry_new2` hands back the sole ownership unit. + unsafe { Self::adopt_ptr(Entry::new2(archive)) } + .expect("archive_entry_new2 returned null") } } impl core::ops::Deref for OwnedEntry { type Target = Entry; #[inline] fn deref(&self) -> &Entry { - // SAFETY: handle is live until Drop; libarchive owns the storage. - unsafe { self.0.as_ref() } - } - } - impl Drop for OwnedEntry { - #[inline] - fn drop(&mut self) { - // SAFETY: handle came from archive_entry_new()/new2() and is freed exactly once. - unsafe { archive_entry_free(self.0.as_ptr()) }; + self.raw() } } @@ -620,7 +601,7 @@ pub mod lib { /// Generic result type used by [`ArchiveIterator`]. pub enum IteratorResult { Err { - archive: *mut Archive, + archive: *mut sys::Archive, message: &'static [u8], }, Result(T), @@ -628,7 +609,7 @@ pub mod lib { impl IteratorResult { #[inline] - pub fn init_err(arch: *mut Archive, msg: &'static [u8]) -> Self { + pub fn init_err(arch: *mut sys::Archive, msg: &'static [u8]) -> Self { Self::Err { message: msg, archive: arch, @@ -643,7 +624,7 @@ pub mod lib { /// Iterates over the entries of an open archive, skipping entries whose /// file kind has its bit set in `filter`. pub struct ArchiveIterator { - pub archive: *mut Archive, + pub archive: *mut sys::Archive, // A u16 bitmask over // `bun_sys::FileKind` variants. pub filter: u16, @@ -660,16 +641,16 @@ pub mod lib { /// /// SAFETY (invariant): `self.archive` is set to a fresh non-null /// handle by `Archive::read_new()` in [`init`] and remains valid - /// until `read_free()` in [`close`]. All `Archive` methods take + /// until `read_free()` in [`close`]. All `sys::Archive` methods take /// `&self` (FFI interior mutability), so a shared borrow suffices. #[inline] - fn archive(&self) -> &Archive { + fn archive(&self) -> &sys::Archive { // SAFETY: see doc comment — non-null for the lifetime of `self`. unsafe { &*self.archive } } pub fn init(tarball_bytes: &[u8]) -> IteratorResult { - let archive = Archive::read_new(); + let archive = sys::Archive::read_new(); // SAFETY: archive_read_new() returns a non-null handle owned by libarchive. let a = unsafe { &*archive }; @@ -766,7 +747,7 @@ pub mod lib { /// live handle this `NextEntry` was yielded from. pub fn read_entry_data( &self, - archive: &Archive, + archive: &sys::Archive, ) -> core::result::Result>, bun_core::OOM> { // SAFETY: self.entry is the libarchive-owned entry from read_next_header. let size = unsafe { (*self.entry).size() }; @@ -790,29 +771,29 @@ pub mod lib { } // ── write-open callback surface (libarchive `archive_write_open2`) ───── - pub type archive_open_callback = unsafe extern "C" fn(*mut Archive, *mut c_void) -> c_int; + pub type archive_open_callback = unsafe extern "C" fn(*mut sys::Archive, *mut c_void) -> c_int; pub type archive_read_callback = - unsafe extern "C" fn(*mut Archive, *mut c_void, *mut *const c_void) -> la_ssize_t; + unsafe extern "C" fn(*mut sys::Archive, *mut c_void, *mut *const c_void) -> la_ssize_t; pub type archive_write_callback = - unsafe extern "C" fn(*mut Archive, *mut c_void, *const c_void, usize) -> la_ssize_t; - pub type archive_close_callback = unsafe extern "C" fn(*mut Archive, *mut c_void) -> c_int; - pub type archive_free_callback = unsafe extern "C" fn(*mut Archive, *mut c_void) -> c_int; + unsafe extern "C" fn(*mut sys::Archive, *mut c_void, *const c_void, usize) -> la_ssize_t; + pub type archive_close_callback = unsafe extern "C" fn(*mut sys::Archive, *mut c_void) -> c_int; + pub type archive_free_callback = unsafe extern "C" fn(*mut sys::Archive, *mut c_void) -> c_int; /// `a` is a live `archive_write_new()` handle. `client_data` is forwarded /// opaquely to the callbacks (never dereferenced here); its lifetime must /// outlast the registered callbacks. #[allow(clippy::not_unsafe_ptr_arg_deref)] pub fn archive_write_open2( - a: &Archive, + a: &sys::Archive, client_data: *mut c_void, open: Option, write: Option, close: Option, free: Option, ) -> c_int { - // SAFETY: `a` is a live handle (`opaque_ffi!` `&self → *mut Self`); - // `client_data` is opaque to libarchive until a callback dereferences it. - unsafe { archive_write_open2_raw(a.as_mut_ptr(), client_data, open, write, close, free) } + // SAFETY: `client_data` is opaque to libarchive until a callback + // dereferences it. + unsafe { archive_write_open2_raw(a, client_data, open, write, close, free) } } /// Growing memory buffer for archive writes with libarchive callbacks. @@ -837,7 +818,7 @@ pub mod lib { } pub unsafe extern "C" fn open_callback( - _a: *mut Archive, + _a: *mut sys::Archive, client_data: *mut c_void, ) -> c_int { // SAFETY: client_data is a *mut GrowingBuffer registered via archive_write_open2. @@ -848,7 +829,7 @@ pub mod lib { } pub unsafe extern "C" fn write_callback( - _a: *mut Archive, + _a: *mut sys::Archive, client_data: *mut c_void, buff: *const c_void, length: usize, @@ -869,7 +850,7 @@ pub mod lib { } pub unsafe extern "C" fn close_callback( - _a: *mut Archive, + _a: *mut sys::Archive, _client_data: *mut c_void, ) -> c_int { 0 @@ -884,7 +865,7 @@ pub mod lib { /// Error payload for [`IterResult`]: the archive handle (for /// `error_string()`) plus a static description. pub struct IteratorError { - pub archive: *mut Archive, + pub archive: *mut sys::Archive, pub message: &'static [u8], } impl IteratorError { @@ -906,6 +887,7 @@ pub mod lib { pub entry: *mut Entry, pub kind: bun_sys::FileKind, } + impl IteratorEntry { /// Borrow the libarchive entry. Valid until the next `next()` call. #[inline] @@ -920,7 +902,7 @@ pub mod lib { /// `archive` is the live handle this entry was yielded from. pub fn read_entry_data( &self, - archive: &Archive, + archive: &sys::Archive, ) -> core::result::Result>, bun_core::OOM> { let size = self.entry().size(); if size < 0 || size > 64 * 1024 * 1024 { @@ -945,7 +927,7 @@ pub mod lib { /// Streaming reader over an in-memory tarball; yields one /// [`IteratorEntry`] per archive entry via [`Iterator::next`]. pub struct Iterator { - pub archive: *mut Archive, + pub archive: *mut sys::Archive, // No filter field: every caller would leave it empty; // re-add if a caller // ever needs it. @@ -955,10 +937,10 @@ pub mod lib { /// /// SAFETY (invariant): `self.archive` is set to a fresh non-null /// handle by `Archive::read_new()` in [`init`] and remains valid - /// until `read_free()` in [`deinit`]. All `Archive` methods take + /// until `read_free()` in [`deinit`]. All `sys::Archive` methods take /// `&self` (FFI interior mutability), so a shared borrow suffices. #[inline] - fn archive(&self) -> &Archive { + fn archive(&self) -> &sys::Archive { // SAFETY: see doc comment — non-null for the lifetime of `self`. unsafe { &*self.archive } } @@ -966,8 +948,8 @@ pub mod lib { /// Opens `tarball_bytes` as a /// gzip-compressed (gnu)tar archive. pub fn init(tarball_bytes: &[u8]) -> IterResult { - let archive = Archive::read_new(); - // SAFETY: `archive` is a fresh non-null `*mut Archive`. + let archive = sys::Archive::read_new(); + // SAFETY: `archive` is a fresh non-null `*mut sys::Archive`. let a = unsafe { &*archive }; match a.read_support_format_tar() { @@ -1074,7 +1056,7 @@ pub mod lib { } } -use lib::Archive; +use lib::{Archive, sys}; #[repr(i32)] // c_int #[derive(Copy, Clone, Eq, PartialEq)] @@ -1091,7 +1073,7 @@ pub struct BufferReadStream { block_size: usize, - archive: *mut Archive, + archive: Archive, reading: bool, } @@ -1117,15 +1099,9 @@ impl BufferReadStream { } /// Borrow the underlying libarchive handle. - /// - /// SAFETY (invariant): `self.archive` is set to a fresh non-null handle by - /// `Archive::read_new()` in `init()` (asserted there) and remains valid - /// until `read_free()` in `Drop`. All `Archive` methods take `&self` - /// (FFI interior mutability), so a shared borrow is sufficient. #[inline] - fn archive(&self) -> &Archive { - // SAFETY: see doc comment — non-null for the lifetime of `self`. - unsafe { &*self.archive } + fn archive(&self) -> &sys::Archive { + &self.archive } /// Borrow the input buffer. @@ -1181,7 +1157,7 @@ impl BufferReadStream { ctx.cast::() } - pub extern "C" fn archive_close_callback(_: *mut Archive, _: *mut c_void) -> c_int { + pub extern "C" fn archive_close_callback(_: *mut sys::Archive, _: *mut c_void) -> c_int { 0 } @@ -1189,7 +1165,7 @@ impl BufferReadStream { /// libarchive C callback: `ctx_` is the `*mut BufferReadStream` registered /// via `archive_read_set_callback_data`; `buffer` is a non-null out-param. pub unsafe extern "C" fn archive_read_callback( - _: *mut Archive, + _: *mut sys::Archive, ctx_: *mut c_void, buffer: *mut *const c_void, ) -> lib::la_ssize_t { @@ -1211,7 +1187,7 @@ impl BufferReadStream { /// libarchive C callback: `ctx_` is the `*mut BufferReadStream` registered /// via `archive_read_set_callback_data`. pub unsafe extern "C" fn archive_skip_callback( - _: *mut Archive, + _: *mut sys::Archive, ctx_: *mut c_void, offset: lib::la_int64_t, ) -> lib::la_int64_t { @@ -1231,7 +1207,7 @@ impl BufferReadStream { /// libarchive C callback: `ctx_` is the `*mut BufferReadStream` registered /// via `archive_read_set_callback_data`. pub unsafe extern "C" fn archive_seek_callback( - _: *mut Archive, + _: *mut sys::Archive, ctx_: *mut c_void, offset: lib::la_int64_t, whence: c_int, @@ -1304,8 +1280,8 @@ impl BufferReadStream { impl Drop for BufferReadStream { fn drop(&mut self) { + // The `archive` field's `Drop` runs next and calls `archive_read_free`. let _ = self.archive().read_close(); - let _ = self.archive().read_free(); } } @@ -1557,7 +1533,7 @@ impl Archiver { // SAFETY: `file_buffer` outlives `stream` (stack-local, dropped at fn exit). let mut stream = unsafe { BufferReadStream::init(file_buffer) }; let _ = stream.open_read(); - let archive = stream.archive; + let archive: &lib::sys::Archive = &stream.archive; // Uses the bun_sys directory-fd helpers (open_dir_absolute / open_dir_at). let dir: Fd = 'brk: { @@ -1581,8 +1557,7 @@ impl Archiver { let _close_dir_guard = scopeguard::guard(dir, |d| d.close()); 'loop_: loop { - // SAFETY: archive valid for stream lifetime - let r = unsafe { (*archive).read_next_header(&mut entry) }; + let r = archive.read_next_header(&mut entry); match r { lib::Result::Eof => break 'loop_, @@ -1693,7 +1668,7 @@ impl Archiver { // SAFETY: `file_buffer` outlives `stream` (stack-local, dropped at fn exit). let mut stream = unsafe { BufferReadStream::init(file_buffer) }; let _ = stream.open_read(); - let archive = stream.archive; + let archive: &lib::sys::Archive = &stream.archive; let mut count: u32 = 0; let dir_fd = dir; @@ -1711,8 +1686,7 @@ impl Archiver { let mut use_lseek = true; 'loop_: loop { - // SAFETY: archive valid for stream lifetime - let r = unsafe { (*archive).read_next_header(&mut entry) }; + let r = archive.read_next_header(&mut entry); match r { lib::Result::Eof => break 'loop_, @@ -2143,12 +2117,8 @@ impl Archiver { plucker_.contents.inflate(size)?; let cap = plucker_.contents.list.capacity(); plucker_.contents.list.resize(cap, 0); - // SAFETY: archive valid - let read = unsafe { - (*archive).read_data( - plucker_.contents.list.as_mut_slice(), - ) - }; + let read = archive + .read_data(plucker_.contents.list.as_mut_slice()); plucker_.contents.inflate( usize::try_from(read).expect("int cast"), )?; @@ -2175,14 +2145,11 @@ impl Archiver { let mut retries_remaining: u8 = 5; 'possibly_retry: while retries_remaining != 0 { - // SAFETY: archive valid - match unsafe { - (*archive).read_data_into_fd( - *file_handle, - &mut use_pwrite, - &mut use_lseek, - ) - } { + match archive.read_data_into_fd( + *file_handle, + &mut use_pwrite, + &mut use_lseek, + ) { lib::Result::Eof => break 'loop_, lib::Result::Ok => break 'possibly_retry, lib::Result::Retry => { @@ -2203,12 +2170,8 @@ impl Archiver { } _ => { if options.log { - // SAFETY: `archive` is the live - // `read_new()` handle this - // extraction loop is iterating. - let archive_error = slice_to_nul( - unsafe { &*archive }.error_string(), - ); + let archive_error = + slice_to_nul(archive.error_string()); Output::err( "libarchive error", "extracting {}: {}", diff --git a/src/libdeflate_sys/libdeflate.rs b/src/libdeflate_sys/libdeflate.rs index 23d1e2534ac7..7ca657e93212 100644 --- a/src/libdeflate_sys/libdeflate.rs +++ b/src/libdeflate_sys/libdeflate.rs @@ -1,6 +1,5 @@ use core::ffi::{c_int, c_uint, c_void}; use core::mem::MaybeUninit; -use core::ptr::NonNull; use std::sync::Once; #[repr(C)] @@ -20,18 +19,37 @@ impl Default for Options { } } +/// The C objects themselves. Only the extern declarations below name these +/// types; all Rust code uses the owning [`Compressor`] / [`Decompressor`] handles. +pub mod sys { + bun_opaque::opaque_ffi! { + /// `struct libdeflate_compressor`. `&Self` is ABI-identical to a + /// non-null `libdeflate_compressor*` and carries no `noalias`/`readonly` + /// — libdeflate mutates the compressor's scratch state through it. + pub struct Compressor; + /// `struct libdeflate_decompressor`. `&Self` is ABI-identical to a + /// non-null `libdeflate_decompressor*` and carries no `noalias`/`readonly` + /// — libdeflate mutates the decompressor's scratch state through it. + pub struct Decompressor; + } +} + unsafe extern "C" { // Allocation: scalar arg, no preconditions; returns null on OOM. - pub(crate) safe fn libdeflate_alloc_compressor(compression_level: c_int) -> *mut Compressor; + pub(crate) safe fn libdeflate_alloc_compressor( + compression_level: c_int, + ) -> *mut sys::Compressor; // NOT safe: `Options` carries caller-supplied `malloc_func`/`free_func` // callbacks that libdeflate will invoke and write through. A bogus callback // (constructible in 100% safe code) would cause UB inside the C library. pub(crate) fn libdeflate_alloc_compressor_ex( compression_level: c_int, options: *const Options, - ) -> *mut Compressor; + ) -> *mut sys::Compressor; + // NOT `safe fn`: `in_`/`out` are raw pointers libdeflate reads/writes. Safe + // Rust can forge a `*const c_void`, so the call carries the obligation. pub(crate) fn libdeflate_deflate_compress( - compressor: *mut Compressor, + compressor: &sys::Compressor, in_: *const c_void, in_nbytes: usize, out: *mut c_void, @@ -39,34 +57,36 @@ unsafe extern "C" { ) -> usize; // Bound queries: opaque handle + scalar. The C API documents `compressor` // may be NULL (returns a library-wide upper bound), so expose it as - // `Option<&mut Compressor>` (NPO-ABI-compatible with `*mut Compressor`). + // `Option<&sys::Compressor>` (NPO-ABI-compatible with the raw pointer). pub(crate) safe fn libdeflate_deflate_compress_bound( - compressor: Option<&mut Compressor>, + compressor: Option<&sys::Compressor>, in_nbytes: usize, ) -> usize; pub(crate) fn libdeflate_zlib_compress( - compressor: *mut Compressor, + compressor: &sys::Compressor, in_: *const c_void, in_nbytes: usize, out: *mut c_void, out_nbytes_avail: usize, ) -> usize; pub(crate) safe fn libdeflate_zlib_compress_bound( - compressor: Option<&mut Compressor>, + compressor: Option<&sys::Compressor>, in_nbytes: usize, ) -> usize; pub(crate) fn libdeflate_gzip_compress( - compressor: *mut Compressor, + compressor: &sys::Compressor, in_: *const c_void, in_nbytes: usize, out: *mut c_void, out_nbytes_avail: usize, ) -> usize; pub(crate) safe fn libdeflate_gzip_compress_bound( - compressor: Option<&mut Compressor>, + compressor: Option<&sys::Compressor>, in_nbytes: usize, ) -> usize; - pub(crate) fn libdeflate_free_compressor(compressor: *mut Compressor); + // safe: C frees the allocation. Freeing is not exclusive access in Rust's + // model, so the receiver is `&`, as `ForeignOwned::release` requires. + pub(crate) safe fn libdeflate_free_compressor(compressor: &sys::Compressor); } fn load_once() { @@ -85,43 +105,49 @@ pub fn load() { LOADED_ONCE.call_once(load_once); } -bun_opaque::opaque_ffi! { - /// Opaque libdeflate compressor handle. `UnsafeCell` makes the type `!Freeze` - /// so a `&Compressor` does not assert immutability of the C-owned state. - pub struct Compressor; +// `libdeflate_alloc_compressor[_ex]` allocates and hands back the object. One +// `Compressor` handle owns exactly that one allocation. +bun_opaque::foreign_handle! { + /// Owned handle to a libdeflate compressor; `Drop` frees it. + /// + /// Every method takes `&self`: `sys::Compressor` is `UnsafeCell`-backed, so a + /// `&` carries no `noalias`/`readonly` and libdeflate freely mutates the + /// compressor's scratch state through it. `#[repr(transparent)]` over `NonNull`, + /// so `Option` is pointer-sized with all-zero = `None`. + pub struct Compressor(sys::Compressor) via libdeflate_free_compressor; } +/// Constructors. libdeflate allocates; each returns an owned handle. impl Compressor { - pub fn alloc(compression_level: c_int) -> *mut Compressor { - libdeflate_alloc_compressor(compression_level) + /// Allocate a compressor at `level` (0..=12). Returns `None` on OOM. + #[inline] + pub fn new(level: c_int) -> Option { + // SAFETY: `libdeflate_alloc_compressor` transfers the sole ownership unit, or null. + unsafe { Self::adopt_ptr(libdeflate_alloc_compressor(level)) } } /// # Safety /// `options.malloc_func`/`free_func` (if set) must be sound allocator /// callbacks — libdeflate writes through their return values. - pub unsafe fn alloc_ex(compression_level: c_int, options: Option<&Options>) -> *mut Compressor { + pub unsafe fn new_ex(level: c_int, options: Option<&Options>) -> Option { // SAFETY: caller upholds the callback contract; `Option<&T>` → `*const T` is NPO-compatible. - unsafe { - libdeflate_alloc_compressor_ex( - compression_level, - options.map_or(core::ptr::null(), |o| o), - ) - } - } - - /// Frees the compressor. `this` must not be used afterward. - pub unsafe fn destroy(this: *mut Compressor) { - // SAFETY: caller guarantees `this` was returned by libdeflate_alloc_compressor[_ex] - // and is not used after this call. - unsafe { libdeflate_free_compressor(this) } + let ptr = unsafe { + libdeflate_alloc_compressor_ex(level, options.map_or(core::ptr::null(), |o| o)) + }; + // SAFETY: `libdeflate_alloc_compressor_ex` transfers the sole ownership unit, or null. + unsafe { Self::adopt_ptr(ptr) } } +} +/// Compression. `&self` throughout: libdeflate mutates through the handle. +impl Compressor { /// Compresses `input` into `output` and returns the number of bytes written. - pub fn inflate(&mut self, input: &[u8], output: &mut [u8]) -> Result { - // SAFETY: self is a valid *mut Compressor; slice ptr/len pairs are valid. + pub fn inflate(&self, input: &[u8], output: &mut [u8]) -> Result { + // SAFETY: slice ptr/len pairs are valid; libdeflate reads `input` and + // writes at most `output.len()` bytes. let written = unsafe { libdeflate_deflate_compress( - self, + self.raw(), input.as_ptr().cast::(), input.len(), output.as_mut_ptr().cast::(), @@ -135,15 +161,15 @@ impl Compressor { } } - pub fn max_bytes_needed(&mut self, input: &[u8], encoding: Encoding) -> usize { + pub fn max_bytes_needed(&self, input: &[u8], encoding: Encoding) -> usize { match encoding { - Encoding::Deflate => libdeflate_deflate_compress_bound(Some(self), input.len()), - Encoding::Zlib => libdeflate_zlib_compress_bound(Some(self), input.len()), - Encoding::Gzip => libdeflate_gzip_compress_bound(Some(self), input.len()), + Encoding::Deflate => libdeflate_deflate_compress_bound(Some(self.raw()), input.len()), + Encoding::Zlib => libdeflate_zlib_compress_bound(Some(self.raw()), input.len()), + Encoding::Gzip => libdeflate_gzip_compress_bound(Some(self.raw()), input.len()), } } - pub fn compress(&mut self, input: &[u8], output: &mut [u8], encoding: Encoding) -> Result { + pub fn compress(&self, input: &[u8], output: &mut [u8], encoding: Encoding) -> Result { match encoding { Encoding::Deflate => self.inflate(input, output), Encoding::Zlib => self.zlib(input, output), @@ -157,7 +183,7 @@ impl Compressor { /// and avoids the UB of materializing `&mut [u8]` over uninitialized bytes. /// On return, `output[..result.written]` is initialized. pub fn compress_into( - &mut self, + &self, input: &[u8], output: &mut [MaybeUninit], encoding: Encoding, @@ -166,15 +192,16 @@ impl Compressor { let in_len = input.len(); let out_ptr = output.as_mut_ptr().cast::(); let out_len = output.len(); - // SAFETY: self is a valid *mut Compressor; ptr/len pairs are valid for the - // FFI contract (input read-only, output write-only for `out_len` bytes). + let this = self.raw(); + // SAFETY: ptr/len pairs are valid for the FFI contract (input read-only, + // output write-only for `out_len` bytes). let written = unsafe { match encoding { Encoding::Deflate => { - libdeflate_deflate_compress(self, in_ptr, in_len, out_ptr, out_len) + libdeflate_deflate_compress(this, in_ptr, in_len, out_ptr, out_len) } - Encoding::Zlib => libdeflate_zlib_compress(self, in_ptr, in_len, out_ptr, out_len), - Encoding::Gzip => libdeflate_gzip_compress(self, in_ptr, in_len, out_ptr, out_len), + Encoding::Zlib => libdeflate_zlib_compress(this, in_ptr, in_len, out_ptr, out_len), + Encoding::Gzip => libdeflate_gzip_compress(this, in_ptr, in_len, out_ptr, out_len), } }; Result { @@ -194,12 +221,7 @@ impl Compressor { /// Safe replacement for the open-coded /// `compress_into(out.spare_capacity_mut()) + unsafe { set_len }` pattern, /// and for the zero-init `vec![0u8; bound]` + `truncate` form. - pub fn compress_to_vec( - &mut self, - input: &[u8], - out: &mut Vec, - encoding: Encoding, - ) -> Result { + pub fn compress_to_vec(&self, input: &[u8], out: &mut Vec, encoding: Encoding) -> Result { let result = self.compress_into(input, out.spare_capacity_mut(), encoding); if result.status == Status::Success { // SAFETY: result.written ≤ spare.len() and libdeflate has @@ -209,11 +231,12 @@ impl Compressor { result } - pub fn zlib(&mut self, input: &[u8], output: &mut [u8]) -> Result { - // SAFETY: self is a valid *mut Compressor; slice ptr/len pairs are valid. + pub fn zlib(&self, input: &[u8], output: &mut [u8]) -> Result { + // SAFETY: slice ptr/len pairs are valid; libdeflate reads `input` and + // writes at most `output.len()` bytes. let result = unsafe { libdeflate_zlib_compress( - self, + self.raw(), input.as_ptr().cast::(), input.len(), output.as_mut_ptr().cast::(), @@ -227,11 +250,12 @@ impl Compressor { } } - pub fn gzip(&mut self, input: &[u8], output: &mut [u8]) -> Result { - // SAFETY: self is a valid *mut Compressor; slice ptr/len pairs are valid. + pub fn gzip(&self, input: &[u8], output: &mut [u8]) -> Result { + // SAFETY: slice ptr/len pairs are valid; libdeflate reads `input` and + // writes at most `output.len()` bytes. let result = unsafe { libdeflate_gzip_compress( - self, + self.raw(), input.as_ptr().cast::(), input.len(), output.as_mut_ptr().cast::(), @@ -246,70 +270,38 @@ impl Compressor { } } -/// Owned RAII libdeflate compressor. Frees on drop. -/// -/// `#[repr(transparent)]` over `NonNull` so `Option` has the -/// same layout as `*mut Compressor` (all-zero = `None`). -#[repr(transparent)] -pub struct OwnedCompressor(NonNull); - -impl OwnedCompressor { - /// Allocate a compressor at `level` (0..=12). Returns `None` on OOM. - #[inline] - pub fn new(level: c_int) -> Option { - NonNull::new(Compressor::alloc(level)).map(Self) - } -} - -impl core::ops::Deref for OwnedCompressor { - type Target = Compressor; - #[inline] - fn deref(&self) -> &Compressor { - // SAFETY: non-null, allocated by libdeflate, exclusively owned by `self`. - unsafe { self.0.as_ref() } - } -} - -impl core::ops::DerefMut for OwnedCompressor { - #[inline] - fn deref_mut(&mut self) -> &mut Compressor { - // SAFETY: non-null, allocated by libdeflate, exclusively owned by `self`. - unsafe { self.0.as_mut() } - } +// `libdeflate_alloc_decompressor[_ex]` allocates and hands back the object. One +// `Decompressor` handle owns exactly that one allocation. +bun_opaque::foreign_handle! { + /// Owned handle to a libdeflate decompressor; `Drop` frees it. + /// + /// Every method takes `&self`: `sys::Decompressor` is `UnsafeCell`-backed, so a + /// `&` carries no `noalias`/`readonly` and libdeflate freely mutates the + /// decompressor's scratch state through it. `#[repr(transparent)]` over `NonNull`, + /// so `Option` is pointer-sized with all-zero = `None`. + pub struct Decompressor(sys::Decompressor) via libdeflate_free_decompressor; } -impl Drop for OwnedCompressor { +/// Constructor. libdeflate allocates; returns an owned handle. +impl Decompressor { + /// Allocate a decompressor. Returns `None` on OOM. #[inline] - fn drop(&mut self) { - // SAFETY: allocated by `libdeflate_alloc_compressor`; freed exactly once here. - unsafe { libdeflate_free_compressor(self.0.as_ptr()) } + pub fn new() -> Option { + // SAFETY: libdeflate_alloc_decompressor transfers a fresh allocation, or null. + unsafe { Self::adopt_ptr(libdeflate_alloc_decompressor()) } } } -bun_opaque::opaque_ffi! { - /// Opaque libdeflate decompressor handle. `UnsafeCell` makes the type `!Freeze`. - pub struct Decompressor; -} - +/// Decompression. `&self` throughout: libdeflate mutates through the handle. impl Decompressor { - pub fn alloc() -> *mut Decompressor { - libdeflate_alloc_decompressor() - } - - /// Frees the decompressor. `this` must not be used afterward. - pub unsafe fn destroy(this: *mut Decompressor) { - // SAFETY: caller guarantees `this` was returned by libdeflate_alloc_decompressor[_ex] - // and is not used after this call. - unsafe { libdeflate_free_decompressor(this) } - } - - pub fn deflate(&mut self, input: &[u8], output: &mut [u8]) -> Result { + pub fn deflate(&self, input: &[u8], output: &mut [u8]) -> Result { let mut actual_in_bytes_ret: usize = input.len(); let mut actual_out_bytes_ret: usize = output.len(); - // SAFETY: self is a valid *mut Decompressor; slice ptr/len pairs and out-params are valid. + // SAFETY: slice ptr/len pairs and out-params are valid; libdeflate reads + // `input` and writes at most `output.len()` bytes. let result = unsafe { libdeflate_deflate_decompress_ex( - self, + self.raw(), input.as_ptr().cast::(), input.len(), output.as_mut_ptr().cast::(), @@ -325,13 +317,14 @@ impl Decompressor { } } - pub fn zlib(&mut self, input: &[u8], output: &mut [u8]) -> Result { + pub fn zlib(&self, input: &[u8], output: &mut [u8]) -> Result { let mut actual_in_bytes_ret: usize = input.len(); let mut actual_out_bytes_ret: usize = output.len(); - // SAFETY: self is a valid *mut Decompressor; slice ptr/len pairs and out-params are valid. + // SAFETY: slice ptr/len pairs and out-params are valid; libdeflate reads + // `input` and writes at most `output.len()` bytes. let result = unsafe { libdeflate_zlib_decompress_ex( - self, + self.raw(), input.as_ptr().cast::(), input.len(), output.as_mut_ptr().cast::(), @@ -347,13 +340,14 @@ impl Decompressor { } } - pub fn gzip(&mut self, input: &[u8], output: &mut [u8]) -> Result { + pub fn gzip(&self, input: &[u8], output: &mut [u8]) -> Result { let mut actual_in_bytes_ret: usize = input.len(); let mut actual_out_bytes_ret: usize = output.len(); - // SAFETY: self is a valid *mut Decompressor; slice ptr/len pairs and out-params are valid. + // SAFETY: slice ptr/len pairs and out-params are valid; libdeflate reads + // `input` and writes at most `output.len()` bytes. let result = unsafe { libdeflate_gzip_decompress_ex( - self, + self.raw(), input.as_ptr().cast::(), input.len(), output.as_mut_ptr().cast::(), @@ -369,7 +363,7 @@ impl Decompressor { } } - pub fn decompress(&mut self, input: &[u8], output: &mut [u8], encoding: Encoding) -> Result { + pub fn decompress(&self, input: &[u8], output: &mut [u8], encoding: Encoding) -> Result { match encoding { Encoding::Deflate => self.deflate(input, output), Encoding::Zlib => self.zlib(input, output), @@ -383,7 +377,7 @@ impl Decompressor { /// and avoids the UB of materializing `&mut [u8]` over uninitialized bytes. /// On `Status::Success`, `output[..result.written]` is initialized. pub fn decompress_into( - &mut self, + &self, input: &[u8], output: &mut [MaybeUninit], encoding: Encoding, @@ -394,13 +388,13 @@ impl Decompressor { let out_len = output.len(); let mut read: usize = in_len; let mut written: usize = out_len; - // SAFETY: self is a valid *mut Decompressor; ptr/len pairs are valid for the - // FFI contract (input read-only, output write-only for `out_len` bytes); - // out-params are valid `*mut usize`. + let this = self.raw(); + // SAFETY: ptr/len pairs are valid for the FFI contract (input read-only, + // output write-only for `out_len` bytes); out-params are valid `*mut usize`. let status = unsafe { match encoding { Encoding::Deflate => libdeflate_deflate_decompress_ex( - self, + this, in_ptr, in_len, out_ptr, @@ -409,7 +403,7 @@ impl Decompressor { &raw mut written, ), Encoding::Zlib => libdeflate_zlib_decompress_ex( - self, + this, in_ptr, in_len, out_ptr, @@ -418,7 +412,7 @@ impl Decompressor { &raw mut written, ), Encoding::Gzip => libdeflate_gzip_decompress_ex( - self, + this, in_ptr, in_len, out_ptr, @@ -448,12 +442,7 @@ impl Decompressor { /// `decompress_into(out.spare_capacity_mut()) + unsafe { set_len }` pattern, /// and for the UB-adjacent `slice_mut(ptr, capacity)` form that materialized /// `&mut [u8]` over uninitialized bytes. - pub fn decompress_to_vec( - &mut self, - input: &[u8], - out: &mut Vec, - encoding: Encoding, - ) -> Result { + pub fn decompress_to_vec(&self, input: &[u8], out: &mut Vec, encoding: Encoding) -> Result { let result = self.decompress_into(input, out.spare_capacity_mut(), encoding); if result.status == Status::Success { // SAFETY: result.written ≤ spare.len() and libdeflate has @@ -471,7 +460,7 @@ impl Decompressor { /// `out.capacity() > max_capacity` (returned as the final /// `InsufficientSpace`). On success, `out.len() == result.written`. pub fn decompress_to_vec_grow( - &mut self, + &self, input: &[u8], out: &mut Vec, encoding: Encoding, @@ -489,46 +478,6 @@ impl Decompressor { } } -/// Owned RAII libdeflate decompressor. Frees on drop. -/// -/// `#[repr(transparent)]` over `NonNull` so `Option` has the -/// same layout as `*mut Decompressor` (all-zero = `None`). -#[repr(transparent)] -pub struct OwnedDecompressor(NonNull); - -impl OwnedDecompressor { - /// Allocate a decompressor. Returns `None` on OOM. - #[inline] - pub fn new() -> Option { - NonNull::new(Decompressor::alloc()).map(Self) - } -} - -impl core::ops::Deref for OwnedDecompressor { - type Target = Decompressor; - #[inline] - fn deref(&self) -> &Decompressor { - // SAFETY: non-null, allocated by libdeflate, exclusively owned by `self`. - unsafe { self.0.as_ref() } - } -} - -impl core::ops::DerefMut for OwnedDecompressor { - #[inline] - fn deref_mut(&mut self) -> &mut Decompressor { - // SAFETY: non-null, allocated by libdeflate, exclusively owned by `self`. - unsafe { self.0.as_mut() } - } -} - -impl Drop for OwnedDecompressor { - #[inline] - fn drop(&mut self) { - // SAFETY: allocated by `libdeflate_alloc_decompressor`; freed exactly once here. - unsafe { libdeflate_free_decompressor(self.0.as_ptr()) } - } -} - pub struct Result { pub read: usize, pub written: usize, @@ -543,9 +492,9 @@ pub enum Encoding { } unsafe extern "C" { - pub(crate) safe fn libdeflate_alloc_decompressor() -> *mut Decompressor; + pub(crate) safe fn libdeflate_alloc_decompressor() -> *mut sys::Decompressor; // NOT safe: `Options` carries allocator callbacks (see `libdeflate_alloc_compressor_ex`). - pub fn libdeflate_alloc_decompressor_ex(options: *const Options) -> *mut Decompressor; + pub fn libdeflate_alloc_decompressor_ex(options: *const Options) -> *mut sys::Decompressor; } pub(crate) const LIBDEFLATE_SUCCESS: c_uint = 0; @@ -564,8 +513,10 @@ pub enum Status { } unsafe extern "C" { + // NOT `safe fn`: `in_`/`out` are raw pointers libdeflate reads/writes. Safe + // Rust can forge a `*const c_void`, so the call carries the obligation. pub fn libdeflate_deflate_decompress( - decompressor: *mut Decompressor, + decompressor: &sys::Decompressor, in_: *const c_void, in_nbytes: usize, out: *mut c_void, @@ -573,7 +524,7 @@ unsafe extern "C" { actual_out_nbytes_ret: *mut usize, ) -> Status; pub(crate) fn libdeflate_deflate_decompress_ex( - decompressor: *mut Decompressor, + decompressor: &sys::Decompressor, in_: *const c_void, in_nbytes: usize, out: *mut c_void, @@ -582,7 +533,7 @@ unsafe extern "C" { actual_out_nbytes_ret: *mut usize, ) -> Status; pub fn libdeflate_zlib_decompress( - decompressor: *mut Decompressor, + decompressor: &sys::Decompressor, in_: *const c_void, in_nbytes: usize, out: *mut c_void, @@ -590,7 +541,7 @@ unsafe extern "C" { actual_out_nbytes_ret: *mut usize, ) -> Status; pub(crate) fn libdeflate_zlib_decompress_ex( - decompressor: *mut Decompressor, + decompressor: &sys::Decompressor, in_: *const c_void, in_nbytes: usize, out: *mut c_void, @@ -599,7 +550,7 @@ unsafe extern "C" { actual_out_nbytes_ret: *mut usize, ) -> Status; pub fn libdeflate_gzip_decompress( - decompressor: *mut Decompressor, + decompressor: &sys::Decompressor, in_: *const c_void, in_nbytes: usize, out: *mut c_void, @@ -607,7 +558,7 @@ unsafe extern "C" { actual_out_nbytes_ret: *mut usize, ) -> Status; pub(crate) fn libdeflate_gzip_decompress_ex( - decompressor: *mut Decompressor, + decompressor: &sys::Decompressor, in_: *const c_void, in_nbytes: usize, out: *mut c_void, @@ -615,7 +566,9 @@ unsafe extern "C" { actual_in_nbytes_ret: *mut usize, actual_out_nbytes_ret: *mut usize, ) -> Status; - pub(crate) fn libdeflate_free_decompressor(decompressor: *mut Decompressor); + // safe: C frees the allocation. Freeing is not exclusive access in Rust's + // model, so the receiver is `&`, as `ForeignOwned::release` requires. + pub(crate) safe fn libdeflate_free_decompressor(decompressor: &sys::Decompressor); pub fn libdeflate_adler32(adler: u32, buffer: *const c_void, len: usize) -> u32; pub fn libdeflate_crc32(crc: u32, buffer: *const c_void, len: usize) -> u32; pub(crate) fn libdeflate_set_memory_allocator( diff --git a/src/mimalloc_sys/mimalloc.rs b/src/mimalloc_sys/mimalloc.rs index ee099698a162..01e689d4cb02 100644 --- a/src/mimalloc_sys/mimalloc.rs +++ b/src/mimalloc_sys/mimalloc.rs @@ -110,29 +110,29 @@ bun_opaque::opaque_ffi! { impl Heap { #[inline] - pub fn delete(&mut self) { - // SAFETY: `self` is a live `*mut Heap` obtained from mimalloc. - unsafe { mi_heap_delete(self) } + pub fn delete(&self) { + // SAFETY: `self` is a live `Heap` obtained from mimalloc. + unsafe { mi_heap_delete(self.as_mut_ptr()) } } #[inline] - pub fn malloc(&mut self, size: usize) -> *mut c_void { - // SAFETY: `self` is a live `*mut Heap` obtained from mimalloc. - unsafe { mi_heap_malloc(self, size) } + pub fn malloc(&self, size: usize) -> *mut c_void { + // SAFETY: `self` is a live `Heap` obtained from mimalloc. + unsafe { mi_heap_malloc(self.as_mut_ptr(), size) } } #[inline] - pub fn calloc(&mut self, count: usize, size: usize) -> *mut c_void { - // SAFETY: `self` is a live `*mut Heap` obtained from mimalloc. - unsafe { mi_heap_calloc(self, count, size) } + pub fn calloc(&self, count: usize, size: usize) -> *mut c_void { + // SAFETY: `self` is a live `Heap` obtained from mimalloc. + unsafe { mi_heap_calloc(self.as_mut_ptr(), count, size) } } /// # Safety /// `p` must be null or a pointer previously allocated by this heap. #[inline] - pub unsafe fn realloc(&mut self, p: *mut c_void, newsize: usize) -> *mut c_void { - // SAFETY: `self` is a live `*mut Heap`; caller upholds `p` contract. - unsafe { mi_heap_realloc(self, p, newsize) } + pub unsafe fn realloc(&self, p: *mut c_void, newsize: usize) -> *mut c_void { + // SAFETY: `self` is a live `Heap`; caller upholds `p` contract. + unsafe { mi_heap_realloc(self.as_mut_ptr(), p, newsize) } } // `p` is only address-range-tested (never dereferenced) — there is no diff --git a/src/opaque/lib.rs b/src/opaque/lib.rs index 0208c2f88a58..90f437743486 100644 --- a/src/opaque/lib.rs +++ b/src/opaque/lib.rs @@ -446,6 +446,34 @@ pub unsafe trait ForeignOwned: Sized { unsafe fn release(this: ::core::ptr::NonNull); } +/// How a [`ForeignRef`] gives its unit back. +/// +/// A foreign object can have more than one ownership discipline: libarchive's +/// `struct archive` is freed by `archive_read_free` when it was opened for +/// reading and `archive_write_free` when opened for writing. [`ForeignOwned`] +/// admits one release per type, so the second discipline names a marker instead. +/// +/// # Safety +/// `release` must give back exactly one unit of `T`, exactly once per handle. +pub unsafe trait ForeignRelease { + /// # Safety + /// `this` must be live and carry a unit the caller is giving up. + unsafe fn release(this: ::core::ptr::NonNull); +} + +/// The release of `T`'s own [`ForeignOwned`] impl. The default for [`ForeignRef`], +/// so `ForeignRef` keeps meaning exactly what it did before markers existed. +pub struct DefaultRelease; + +// SAFETY: forwards to `T`'s own release, whose contract is identical. +unsafe impl ForeignRelease for DefaultRelease { + #[inline(always)] + unsafe fn release(this: ::core::ptr::NonNull) { + // SAFETY: caller gives up the unit. + unsafe { T::release(this) } + } +} + /// Owned handle to a foreign object. /// /// `T` : `ForeignRef` :: `Path` : `PathBuf` — the borrowed opaque and its @@ -458,10 +486,19 @@ pub unsafe trait ForeignOwned: Sized { /// /// Not `Clone` — duplicating an ownership unit is a per-type decision. Not /// `Send`/`Sync` — inherited from `NonNull`. +/// `R` selects the release; it defaults to `T`'s own [`ForeignOwned`] impl, so +/// `ForeignRef` needs no marker. Name one only for a foreign object with two +/// ownership disciplines — see [`foreign_release!`]. #[repr(transparent)] -pub struct ForeignRef(::core::ptr::NonNull); +pub struct ForeignRef +where + R: ForeignRelease, +{ + ptr: ::core::ptr::NonNull, + _r: ::core::marker::PhantomData, +} -impl ForeignRef { +impl> ForeignRef { /// Adopt an ownership unit the caller is transferring in. /// /// # Safety @@ -469,17 +506,20 @@ impl ForeignRef { /// handle will give back. #[inline(always)] pub const unsafe fn adopt(ptr: ::core::ptr::NonNull) -> Self { - Self(ptr) + Self { + ptr, + _r: ::core::marker::PhantomData, + } } #[inline(always)] pub const fn as_ptr(&self) -> *mut T { - self.0.as_ptr() + self.ptr.as_ptr() } #[inline(always)] pub const fn as_non_null(&self) -> ::core::ptr::NonNull { - self.0 + self.ptr } /// Hand the ownership unit to a foreign owner (a callback context, a C++ @@ -488,24 +528,24 @@ impl ForeignRef { pub fn leak(self) -> ::core::ptr::NonNull { // `ManuallyDrop`, not `mem::forget`: `clippy::mem_forget` is denied here, // and forgetting a `Drop` type reads as a bug even when it is the point. - ::core::mem::ManuallyDrop::new(self).0 + ::core::mem::ManuallyDrop::new(self).ptr } } -impl ::core::ops::Deref for ForeignRef { +impl> ::core::ops::Deref for ForeignRef { type Target = T; #[inline(always)] fn deref(&self) -> &T { - // SAFETY: `self.0` is non-null and live for `self`'s lifetime. - unsafe { opaque_deref_nn(self.0.as_ptr()) } + // SAFETY: `self.ptr` is non-null and live for `self`'s lifetime. + unsafe { opaque_deref_nn(self.ptr.as_ptr()) } } } -impl Drop for ForeignRef { +impl> Drop for ForeignRef { #[inline(always)] fn drop(&mut self) { // SAFETY: we own exactly one unit; `adopt`/`leak` maintain that. - unsafe { T::release(self.0) } + unsafe { R::release(self.ptr) } } } @@ -531,3 +571,116 @@ macro_rules! foreign_owned { } }; } + +/// Declare a *second* release discipline for an already-[`ForeignOwned`] type. +/// +/// `ForeignOwned` admits one release per type. A foreign object with two — a +/// libarchive `struct archive` freed by `archive_read_free` or +/// `archive_write_free` depending on how it was opened — names a marker for the +/// other, and spells the handle `ForeignRef`. +/// +/// ```ignore +/// foreign_release!(pub WriteFree => sys::Archive, archive_write_free_release); +/// pub struct WriteArchive(bun_opaque::ForeignRef); +/// ``` +#[macro_export] +macro_rules! foreign_release { + ($(#[$m:meta])* $v:vis $marker:ident => $t:ty, $release:path) => { + $(#[$m])* + $v enum $marker {} + // SAFETY: `$release` gives back exactly one ownership unit of `$t`. + unsafe impl $crate::ForeignRelease<$t> for $marker { + #[inline(always)] + unsafe fn release(this: ::core::ptr::NonNull<$t>) { + $release($crate::opaque_deref(this.as_ptr())) + } + } + }; +} + +/// Emit an owning handle over an [`opaque_ffi!`] ZST: the newtype, its +/// `ForeignOwned` impl, and the ownership plumbing every handle needs. +/// +/// Hand-writing `adopt`/`adopt_ptr`/`as_ptr`/`leak`/`raw` per type means a +/// missing `mem::forget` in one copy is a double-free the other copies do not +/// reveal. Declare inherent methods in a separate `impl` block as usual. +/// +/// ```ignore +/// opaque_ffi! { pub struct FetchHeaders; } // in `mod sys` +/// foreign_handle! { +/// /// Owned handle to a C++ `WebCore::FetchHeaders`. +/// pub struct FetchHeaders(sys::FetchHeaders) via WebCore__FetchHeaders__deref; +/// } +/// ``` +/// +/// For a type with a second discipline, name the marker instead: +/// ```ignore +/// foreign_release!(pub WriteFree => sys::Archive, archive_write_free_release); +/// foreign_handle! { pub struct WriteArchive(sys::Archive) via marker WriteFree; } +/// ``` +#[macro_export] +macro_rules! foreign_handle { + ($(#[$m:meta])* $v:vis struct $name:ident($sys:ty) via $release:path;) => { + $crate::foreign_owned!($sys, $release); + $crate::__foreign_handle!($(#[$m])* $v $name, $sys, $crate::ForeignRef<$sys>); + }; + ($(#[$m:meta])* $v:vis struct $name:ident($sys:ty) via marker $marker:ty;) => { + $crate::__foreign_handle!($(#[$m])* $v $name, $sys, $crate::ForeignRef<$sys, $marker>); + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __foreign_handle { + ($(#[$m:meta])* $v:vis $name:ident, $sys:ty, $inner:ty) => { + $(#[$m])* + #[repr(transparent)] + $v struct $name($inner); + + impl $name { + /// Adopt an ownership unit the caller is transferring in. + /// + /// # Safety + /// `ptr` must be live and carry the sole unit; no other handle may + /// give it back. + #[inline] + #[allow(dead_code)] + $v unsafe fn adopt(ptr: ::core::ptr::NonNull<$sys>) -> Self { + // SAFETY: caller transfers the sole unit. + Self(unsafe { <$inner>::adopt(ptr) }) + } + + /// Adopt a nullable owning pointer; `None` on null. + /// + /// # Safety + /// A non-null `ptr` must satisfy [`Self::adopt`]'s contract. + #[inline] + #[allow(dead_code)] + $v unsafe fn adopt_ptr(ptr: *mut $sys) -> ::core::option::Option { + // SAFETY: caller contract. + ::core::ptr::NonNull::new(ptr).map(|p| unsafe { Self::adopt(p) }) + } + + /// The foreign pointer, still owned by `self`. + #[inline] + #[allow(dead_code)] + $v fn as_ptr(&self) -> *mut $sys { + self.0.as_ptr() + } + + /// Hand the unit to a foreign owner. Pairs with a later [`Self::adopt`]. + #[inline] + #[allow(dead_code)] + $v fn leak(self) -> ::core::ptr::NonNull<$sys> { + self.0.leak() + } + + /// Borrow the foreign object. `&$sys` carries no `noalias`. + #[inline] + #[allow(dead_code)] + fn raw(&self) -> &$sys { + &self.0 + } + } + }; +} diff --git a/src/options_types/context.rs b/src/options_types/context.rs index 71b04ade4034..5a7066e4b68b 100644 --- a/src/options_types/context.rs +++ b/src/options_types/context.rs @@ -416,7 +416,7 @@ pub struct TestOptions { // back-edge. High tier owns construction/destruction; this field only // stores the pointer. LIFETIMES.tsv says OWNED, so the high-tier setter is // responsible for freeing any previous value. - pub test_filter_regex: Option>, // SAFETY: erased *mut bun_jsc::RegularExpression + pub test_filter_regex: Option>, // SAFETY: erased *mut bun_jsc::regular_expression::sys::RegularExpression pub max_concurrency: u32, /// `bun test --isolate`: run each test file in a fresh global object on /// the same VM, force-closing leaked handles between files. @@ -459,8 +459,9 @@ pub struct Reporters { } impl TestOptions { - /// Returns the erased `*mut bun_jsc::RegularExpression`. Caller (high tier) - /// casts back: `unsafe { &*ptr.cast::() }`. + /// Returns the erased pointer to the C++ regex. Caller (high tier) casts back to + /// `NonNull` and borrows it + /// with `RegularExpression::borrow_leaked` - never to the owning handle. #[inline] pub fn test_filter_regex(&self) -> Option> { // SAFETY: erased bun_jsc::RegularExpression — see field decl. diff --git a/src/runtime/api/Archive.rs b/src/runtime/api/Archive.rs index 3058bae6ddd2..cb05b6479be5 100644 --- a/src/runtime/api/Archive.rs +++ b/src/runtime/api/Archive.rs @@ -130,7 +130,7 @@ impl Archive { } /// Configure archive for reading tar/tar.gz -fn configure_archive_reader(archive: &libarchive::lib::Archive) { +fn configure_archive_reader(archive: &libarchive::lib::sys::Archive) { let _ = archive.read_support_format_tar(); let _ = archive.read_support_format_gnutar(); let _ = archive.read_support_filter_gzip(); @@ -148,7 +148,7 @@ fn entry_pathname_utf8(entry: &libarchive::lib::Entry) -> Result, bun_al /// Count the number of files in an archive fn count_files_in_archive(data: &[u8]) -> u32 { use libarchive::lib; - let archive = lib::ReadArchive::new(); + let archive = lib::Archive::read_new(); configure_archive_reader(&archive); if archive.read_open_memory(data) != lib::Result::Ok { @@ -302,7 +302,7 @@ fn build_tarball_from_object(global: &JSGlobalObject, obj: JSValue) -> JsResult< // errdefer growing_buffer.deinit() — handled by Drop on Vec let archive = lib::WriteArchive::new(); - let archive_ref: &lib::Archive = &archive; + let archive_ref: &lib::sys::Archive = &archive; if archive_ref.write_set_format_pax_restricted() != lib::Result::Ok { return Err(global.throw_invalid_arguments(format_args!( @@ -1140,7 +1140,7 @@ pub struct FilesContext { } impl FilesContext { - fn clone_error_string(archive: &libarchive::lib::Archive) -> Option { + fn clone_error_string(archive: &libarchive::lib::sys::Archive) -> Option { let err_str = archive.error_string(); if err_str.is_empty() { return None; @@ -1150,7 +1150,7 @@ impl FilesContext { fn do_run(&mut self) -> Result { use libarchive::lib; - let archive = lib::ReadArchive::new(); + let archive = lib::Archive::read_new(); configure_archive_reader(&archive); if archive.read_open_memory(self.store.shared_view()) != lib::Result::Ok { @@ -1343,8 +1343,8 @@ fn compress_gzip(data: &[u8], level: u8) -> Result, CompressError> { use bun_libdeflate_sys::libdeflate; libdeflate::load(); - let mut compressor = - libdeflate::OwnedCompressor::new(i32::from(level)).ok_or(CompressError::GzipInitFailed)?; + let compressor = + libdeflate::Compressor::new(i32::from(level)).ok_or(CompressError::GzipInitFailed)?; let max_size = compressor.max_bytes_needed(data, libdeflate::Encoding::Gzip); @@ -1429,7 +1429,7 @@ fn extract_to_disk_filtered( glob_patterns: Option<&[Box<[u8]>]>, ) -> Result { use libarchive::lib; - let archive = lib::ReadArchive::new(); + let archive = lib::Archive::read_new(); configure_archive_reader(&archive); if archive.read_open_memory(file_buffer) != lib::Result::Ok { diff --git a/src/runtime/api/BunObject.rs b/src/runtime/api/BunObject.rs index d1adbce1748e..fe6750b4bc82 100644 --- a/src/runtime/api/BunObject.rs +++ b/src/runtime/api/BunObject.rs @@ -2562,7 +2562,7 @@ pub mod JSZlib { leak_list_into_uint8array(global_this, list) } Library::Libdeflate => { - let Some(mut decompressor) = bun_libdeflate::OwnedDecompressor::new() else { + let Some(decompressor) = bun_libdeflate::Decompressor::new() else { drop(list); return Err(global_this.throw_out_of_memory()); }; @@ -2701,8 +2701,7 @@ pub mod JSZlib { leak_list_into_uint8array(global_this, list) } Library::Libdeflate => { - let Some(mut compressor) = bun_libdeflate::OwnedCompressor::new(level.unwrap_or(6)) - else { + let Some(compressor) = bun_libdeflate::Compressor::new(level.unwrap_or(6)) else { return Err(global_this.throw_out_of_memory()); }; let encoding = if is_gzip { diff --git a/src/runtime/api/bun/SSLContextCache.rs b/src/runtime/api/bun/SSLContextCache.rs index a85b8eb4a34a..bb5ffa8f3c72 100644 --- a/src/runtime/api/bun/SSLContextCache.rs +++ b/src/runtime/api/bun/SSLContextCache.rs @@ -63,7 +63,7 @@ pub struct Entry { /// Nulled by `bun_ssl_ctx_cache_on_free` when BoringSSL drops the last /// ref. Tombstoned entries are reclaimed on the next `get_or_create` for /// the same digest, or by the periodic compact. - pub ctx: Cell<*mut boringssl::SSL_CTX>, + pub ctx: Cell<*mut boringssl::sys::SSL_CTX>, /// BACKREF: the cache outlives every `Entry` it allocates (Drop clears /// ex_data first so the `CRYPTO_EX_free` callback never sees a dangling /// owner). @@ -76,7 +76,7 @@ impl SSLContextCache { &mut self, config: &SSLConfig, err: &mut create_bun_socket_error_t, - ) -> Option<*mut boringssl::SSL_CTX> { + ) -> Option<*mut boringssl::sys::SSL_CTX> { let opts = config.as_usockets(); self.get_or_create_digest(&opts, opts.digest(), err) } @@ -87,7 +87,7 @@ impl SSLContextCache { &mut self, opts: &uws::SocketContext::BunSocketContextOptions, err: &mut create_bun_socket_error_t, - ) -> Option<*mut boringssl::SSL_CTX> { + ) -> Option<*mut boringssl::sys::SSL_CTX> { self.get_or_create_digest(opts, opts.digest(), err) } @@ -99,7 +99,7 @@ impl SSLContextCache { opts: &uws::SocketContext::BunSocketContextOptions, d: Digest, err: &mut create_bun_socket_error_t, - ) -> Option<*mut boringssl::SSL_CTX> { + ) -> Option<*mut boringssl::sys::SSL_CTX> { { let _guard = self.mutex.lock_guard(); if let Some(entry) = self.map.get(&d) { @@ -108,8 +108,8 @@ impl SSLContextCache { let entry = unsafe { &**entry }; let ctx = entry.ctx.get(); if !ctx.is_null() { - // SAFETY: ctx non-null and tombstone write is serialized by this mutex. - unsafe { boringssl::SSL_CTX_up_ref(ctx) }; + // The tombstone write is serialized by this mutex. + boringssl::SSL_CTX_up_ref(boringssl::sys::SSL_CTX::opaque_ref(ctx)); return Some(ctx); } } @@ -139,21 +139,20 @@ impl SSLContextCache { // `bun_ssl_ctx_cache_on_free` and write this same `ctx` cell. let existing = entry.ctx.get(); if !existing.is_null() { - // SAFETY: existing non-null; ctx is the fresh CTX we just built and own. - unsafe { - boringssl::SSL_CTX_up_ref(existing); - boringssl::SSL_CTX_free(ctx); - } + // `ctx` is the fresh CTX we just built and own; give it back. + boringssl::SSL_CTX_up_ref(boringssl::sys::SSL_CTX::opaque_ref(existing)); + boringssl::SSL_CTX_free(boringssl::sys::SSL_CTX::opaque_ref(ctx)); return Some(existing); } // Tombstone — adopt the rebuilt CTX into the existing slot. // SSL_CTX_set_ex_data only fails on OOM (Bun crashes anyway), but if // it did, the entry would never tombstone and `entry.ctx` would dangle // after the CTX is freed. Don't cache it; caller still owns the ref. - // SAFETY: ctx is a valid SSL_CTX*; entry_ptr is a valid heap pointer. + // SAFETY: entry_ptr is a valid heap pointer; the CRYPTO_EX_free + // callback owns the eventual deref. if unsafe { boringssl::SSL_CTX_set_ex_data( - ctx, + boringssl::sys::SSL_CTX::opaque_ref(ctx), c::us_ssl_ctx_cache_ex_idx(), entry_ptr.cast::(), ) @@ -170,10 +169,11 @@ impl SSLContextCache { owner: owner_ptr, })); *gop.value_ptr = entry; - // SAFETY: ctx is a valid SSL_CTX*; entry is a fresh non-null heap pointer. + // SAFETY: entry is a fresh non-null heap pointer; the CRYPTO_EX_free + // callback owns the eventual deref. if unsafe { boringssl::SSL_CTX_set_ex_data( - ctx, + boringssl::sys::SSL_CTX::opaque_ref(ctx), c::us_ssl_ctx_cache_ex_idx(), entry.cast::(), ) @@ -257,10 +257,10 @@ impl Drop for SSLContextCache { let e = unsafe { &*entry }; let ctx = e.ctx.get(); if !ctx.is_null() { - // SAFETY: ctx non-null; clearing the ex_data slot we set. + // SAFETY: clearing the ex_data slot we set; null payload. unsafe { boringssl::SSL_CTX_set_ex_data( - ctx, + boringssl::sys::SSL_CTX::opaque_ref(ctx), c::us_ssl_ctx_cache_ex_idx(), ptr::null_mut(), ); diff --git a/src/runtime/api/bun/SecureContext.rs b/src/runtime/api/bun/SecureContext.rs index d09aadc1279b..26df1df14082 100644 --- a/src/runtime/api/bun/SecureContext.rs +++ b/src/runtime/api/bun/SecureContext.rs @@ -33,7 +33,7 @@ pub use crate::generated_classes::js_SecureContext as js; #[bun_jsc::JsClass] #[repr(C)] pub struct SecureContext { - pub ctx: *mut boringssl::SSL_CTX, + pub ctx: *mut boringssl::sys::SSL_CTX, /// `BunSocketContextOptions.digest()` — exactly the fields that reach /// `us_ssl_ctx_from_options`. Stored so an `intern()` WeakGCMap hit (keyed by /// the low 64 bits) can do a full content-equality check before reusing. @@ -331,11 +331,8 @@ impl SecureContext { /// `SSL_CTX_up_ref` and return — for callers that want to outlive this /// wrapper's GC. Most paths just pass `this.ctx` directly and let `SSL_new` /// take its own ref. - pub fn borrow(&self) -> *mut boringssl::SSL_CTX { - unsafe { - // SAFETY: self.ctx is a valid SSL_CTX* held for the lifetime of this wrapper. - let _ = boringssl::SSL_CTX_up_ref(self.ctx); - } + pub fn borrow(&self) -> *mut boringssl::sys::SSL_CTX { + let _ = boringssl::SSL_CTX_up_ref(boringssl::sys::SSL_CTX::opaque_ref(self.ctx)); self.ctx } @@ -380,8 +377,8 @@ impl SecureContext { // false positive on that contract. #[allow(clippy::boxed_local)] pub fn finalize(self: Box) { - // SAFETY: `ctx` was created by `SSL_CTX_new`; freed exactly once here. - unsafe { boringssl::SSL_CTX_free(self.ctx) }; + // `ctx` was created by `SSL_CTX_new`; released exactly once here. + boringssl::SSL_CTX_free(boringssl::sys::SSL_CTX::opaque_ref(self.ctx)); } pub fn memory_cost(&self) -> usize { diff --git a/src/runtime/api/bun/x509.rs b/src/runtime/api/bun/x509.rs index cbc505acda27..0ca99135592b 100644 --- a/src/runtime/api/bun/x509.rs +++ b/src/runtime/api/bun/x509.rs @@ -1,24 +1,29 @@ -use bun_boringssl_sys::X509; +use bun_boringssl_sys::{X509, sys}; use bun_jsc::{JSGlobalObject, JSValue, JsResult}; pub use bun_boringssl::x509::is_safe_alt_name; -pub fn to_js(cert: &mut X509, global_object: &JSGlobalObject) -> JsResult { +/// Borrows `cert`: C++ wraps the pointer in a non-owning `ncrypto::X509View` +/// (`Bun__X509__toJSLegacyEncoding`), so the caller keeps its reference. +pub fn to_js(cert: &mut sys::X509, global_object: &JSGlobalObject) -> JsResult { bun_jsc::from_js_host_call(global_object, || { Bun__X509__toJSLegacyEncoding(cert, global_object) }) } -pub(crate) fn to_js_object(cert: &mut X509, global_object: &JSGlobalObject) -> JsResult { +/// Consumes `cert`: C++ moves the pointer into an owning `ncrypto::X509Pointer` +/// (`Bun__X509__toJS`), so the handle's ref is leaked here rather than released. +pub(crate) fn to_js_object(cert: X509, global_object: &JSGlobalObject) -> JsResult { + let cert = sys::X509::opaque_mut(cert.leak().as_ptr()); Ok(Bun__X509__toJS(cert, global_object)) } -// `X509`/`JSGlobalObject` are opaque `repr(C)` handles; `&mut`/`&` are +// `sys::X509`/`JSGlobalObject` are opaque `repr(C)` handles; `&mut`/`&` are // ABI-identical to non-null pointers, so the validity proof is in the type. unsafe extern "C" { safe fn Bun__X509__toJSLegacyEncoding( - cert: &mut X509, + cert: &mut sys::X509, global_object: &JSGlobalObject, ) -> JSValue; - safe fn Bun__X509__toJS(cert: &mut X509, global_object: &JSGlobalObject) -> JSValue; + safe fn Bun__X509__toJS(cert: &mut sys::X509, global_object: &JSGlobalObject) -> JSValue; } diff --git a/src/runtime/api/filesystem_router.rs b/src/runtime/api/filesystem_router.rs index 3a83df91411d..7832314c3b6a 100644 --- a/src/runtime/api/filesystem_router.rs +++ b/src/runtime/api/filesystem_router.rs @@ -901,7 +901,7 @@ impl MatchedRoute { query: &'a mut QueryStringMap, } impl<'a> ObjectInitializer for QueryObjectCreator<'a> { - fn create(&mut self, obj: &mut JSObject, global: &JSGlobalObject) -> JsResult<()> { + fn create(&mut self, obj: &JSObject, global: &JSGlobalObject) -> JsResult<()> { // Stack scratch — 256 × 16-byte fat ptr × 2 ≈ 8 KiB, well within Bun's // JS-thread stack budget. A `RefCell<[&'static [u8]; 256]>` TLS slot // would be unsound: `iter.next()` writes QueryStringMap-lifetime diff --git a/src/runtime/bake/DevServer.rs b/src/runtime/bake/DevServer.rs index cafe81c95e22..e2f22e196489 100644 --- a/src/runtime/bake/DevServer.rs +++ b/src/runtime/bake/DevServer.rs @@ -1706,7 +1706,10 @@ impl bun_uws_sys::web_socket::WebSocketUpgradeServer for D } let dw = bun_core::heap::into_raw(HmrSocket::new(this, res)); let _ = this.active_websocket_connections.insert(dw, ()); - let _ = res.upgrade( + // Fully qualified: `ResponseLike::upgrade` is also in scope for this type + // and would box `dw` a second time. The inherent one takes the raw pointer. + let _ = bun_uws_sys::NewAppResponse::::upgrade( + res, dw, req.header(b"sec-websocket-key").unwrap_or(b""), req.header(b"sec-websocket-protocol").unwrap_or(b""), diff --git a/src/runtime/cli/Arguments.rs b/src/runtime/cli/Arguments.rs index 65d20886727c..a3013b308aeb 100644 --- a/src/runtime/cli/Arguments.rs +++ b/src/runtime/cli/Arguments.rs @@ -1730,8 +1730,8 @@ fn parse_test_command_options(args: &clap::Args, ctx: Context<'_>) { }; // The compiled regex lives in `bun_jsc::RegularExpression` (T6); the // T3 `TestOptions` field is type-erased to `NonNull<()>` to break the - // back-edge. High tier owns construction/destruction. - ctx.test_options.test_filter_regex = core::ptr::NonNull::new(regex.cast::<()>()); + // back-edge. `leak()` hands it the allocation for the process lifetime. + ctx.test_options.test_filter_regex = Some(regex.leak().cast::<()>()); } if let Some(since) = args.option(b"--changed") { ctx.test_options.changed = Some(since.into()); diff --git a/src/runtime/cli/audit_command.rs b/src/runtime/cli/audit_command.rs index 62c33d490159..8d80334c8122 100644 --- a/src/runtime/cli/audit_command.rs +++ b/src/runtime/cli/audit_command.rs @@ -425,7 +425,7 @@ fn send_audit_request( body: &[u8], ) -> Result, bun_alloc::AllocError> { libdeflate::load(); - let mut compressor = libdeflate::OwnedCompressor::new(6).ok_or(bun_alloc::AllocError)?; + let compressor = libdeflate::Compressor::new(6).ok_or(bun_alloc::AllocError)?; let max_compressed_size = compressor.max_bytes_needed(body, libdeflate::Encoding::Gzip); let mut compressed_body = Vec::with_capacity(max_compressed_size); diff --git a/src/runtime/cli/pack_command.rs b/src/runtime/cli/pack_command.rs index 69db13754e90..2306a01eca6a 100644 --- a/src/runtime/cli/pack_command.rs +++ b/src/runtime/cli/pack_command.rs @@ -19,7 +19,7 @@ use bun_parsers::json as JSON; // lift via `bun_ast::Expr::from(t2_expr)` at the call site. use bun_ast::{E, Expr, ExprData}; use bun_js_printer as js_printer; -use bun_libarchive::lib::{Archive, Entry as ArchiveEntry, Result as ArchiveStatus}; +use bun_libarchive::lib::{Entry as ArchiveEntry, Result as ArchiveStatus, sys::Archive}; use bun_paths::{self as path, PathBuffer, SEP_STR}; // `bun.ptr.CowString = CowSlice(u8)` — the lifetime-free struct port (init_owned/ // borrow_subslice/length live on `cow_slice::CowSliceZ`, not on the `std::borrow::Cow` diff --git a/src/runtime/cli/publish_command.rs b/src/runtime/cli/publish_command.rs index d4adc2db5868..59f16b4bcea9 100644 --- a/src/runtime/cli/publish_command.rs +++ b/src/runtime/cli/publish_command.rs @@ -12,7 +12,7 @@ use bun_dotenv as dotenv; use bun_http as http; use bun_install::lockfile::{LoadResult, LoadStep}; use bun_install::{self as install, Lockfile, Npm, PackageManager, Subcommand}; -use bun_libarchive::lib::{Archive, ArchiveIterator, IteratorResult as ArchiveIterResult}; +use bun_libarchive::lib::{ArchiveIterator, IteratorResult as ArchiveIterResult, sys::Archive}; use bun_parsers::json as json_mod; use bun_paths::resolve_path::{join_abs_string_buf_z, normalize_buf, normalize_buf_z}; use bun_paths::{self as path, PathBuffer}; diff --git a/src/runtime/cli/test_command.rs b/src/runtime/cli/test_command.rs index 2fce124695cf..257380998a00 100644 --- a/src/runtime/cli/test_command.rs +++ b/src/runtime/cli/test_command.rs @@ -2084,14 +2084,13 @@ impl TestCommand { only: ctx.test_options.only, bail: ctx.test_options.bail, max_concurrency: ctx.test_options.max_concurrency, - // `test_filter_regex` is an erased `*mut RegularExpression` (see - // options_types::context); cast back to a typed `NonNull` — - // kept raw so `matches()` can write through it without - // laundering shared-ref provenance. + // `test_filter_regex` is an erased pointer to the C++ regex (see + // options_types::context); cast back to the `sys` type, not the + // owning handle. We only borrow it - `Arguments` leaked it. filter_regex: ctx .test_options .test_filter_regex() - .map(|p| p.cast::()), + .map(|p| p.cast::()), snapshots: Snapshots { update_snapshots: ctx.test_options.update_snapshots, total: 0, diff --git a/src/runtime/crypto/CryptoHasher.rs b/src/runtime/crypto/CryptoHasher.rs index eb19f059a3f7..4fa20e19016a 100644 --- a/src/runtime/crypto/CryptoHasher.rs +++ b/src/runtime/crypto/CryptoHasher.rs @@ -26,18 +26,14 @@ type Digest = evp::Digest; const EVP_MAX_MD_SIZE_USIZE: usize = boring_ssl::EVP_MAX_MD_SIZE as usize; /// Local helper: dereference the raw `*mut VirtualMachine` to reach -/// `RareData::boring_engine()` and cast the bun_jsc-local opaque `ENGINE` -/// to the real `bun_boringssl_sys::ENGINE` (both name the same C struct). +/// `RareData::boring_engine()`, which borrows the VM-owned `ENGINE` out of the +/// owning `bun_boringssl_sys::ENGINE` handle stored on `RareData`. The result is +/// the C object, never the handle — nothing here releases a ref. #[inline] -fn boring_engine(global: &JSGlobalObject) -> *mut boring_ssl::ENGINE { +fn boring_engine(global: &JSGlobalObject) -> *mut boring_ssl::sys::ENGINE { // SAFETY: `bun_vm()` returns the raw `*mut VirtualMachine` for a Bun-owned // global (never null, single-threaded JS heap), so deref-to-&mut is sound here. - global - .bun_vm() - .as_mut() - .rare_data() - .boring_engine() - .cast::() + global.bun_vm().as_mut().rare_data().boring_engine() } /// Local helper replacing `input == .blob && input.blob.isBunFile()`. @@ -1104,7 +1100,7 @@ pub trait StaticHasher: 'static { fn final_(&mut self, out: &mut Self::Digest); /// # Safety /// `engine` must be null (default engine) or a live `ENGINE*`. - unsafe fn hash(input: &[u8], out: &mut Self::Digest, engine: *mut boring_ssl::ENGINE); + unsafe fn hash(input: &[u8], out: &mut Self::Digest, engine: *mut boring_ssl::sys::ENGINE); /// Per-monomorphization codegen module (`bun_jsc::generated::JS${NAME}`); /// each `impl_static_hasher!` arm binds to the typed wrapper exported by /// `js_class_module!` for its concrete name. @@ -1139,9 +1135,14 @@ macro_rules! impl_static_hasher { <$ty>::r#final(self, out) } #[inline] - unsafe fn hash(input: &[u8], out: &mut Self::Digest, engine: *mut boring_ssl::ENGINE) { - // `bun_sha_hmac::sha::ffi::ENGINE` re-exports `bun_boringssl_sys::ENGINE`, - // so the VM-owned engine pointer threads through without a cast. + unsafe fn hash( + input: &[u8], + out: &mut Self::Digest, + engine: *mut boring_ssl::sys::ENGINE, + ) { + // `bun_sha_hmac::sha::ffi::ENGINE` re-exports + // `bun_boringssl_sys::sys::ENGINE`, so the VM-owned engine pointer + // threads through without a cast. // SAFETY: caller upholds `engine` validity (forwarded). unsafe { <$ty>::hash(input, out, engine) } } diff --git a/src/runtime/crypto/EVP.rs b/src/runtime/crypto/EVP.rs index 74767e0d1760..6514947c14bb 100644 --- a/src/runtime/crypto/EVP.rs +++ b/src/runtime/crypto/EVP.rs @@ -170,7 +170,7 @@ impl EVP { pub fn init( algorithm: Algorithm, md: *const boringssl::EVP_MD, - engine: *mut boringssl::ENGINE, + engine: *mut boringssl::sys::ENGINE, ) -> EVP { bun_boringssl::load(); @@ -188,7 +188,7 @@ impl EVP { /// `engine` must be either null or a valid `ENGINE` pointer. // Forwards `engine` to BoringSSL without dereferencing; not_unsafe_ptr_arg_deref is a false positive on opaque-token forwarding. #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub fn reset(&mut self, engine: *mut boringssl::ENGINE) { + pub fn reset(&mut self, engine: *mut boringssl::sys::ENGINE) { // SAFETY: FFI into BoringSSL; ERR_clear_error has no preconditions. self.ctx was // initialized in init() and remains valid for the lifetime of EVP; self.md is a // static singleton. @@ -204,7 +204,7 @@ impl EVP { #[allow(clippy::not_unsafe_ptr_arg_deref)] pub fn hash( &mut self, - engine: *mut boringssl::ENGINE, + engine: *mut boringssl::sys::ENGINE, input: &[u8], output: &mut [u8], ) -> Option { @@ -232,7 +232,7 @@ impl EVP { /// `engine` must be either null or a valid `ENGINE` pointer. pub fn r#final<'a>( &mut self, - engine: *mut boringssl::ENGINE, + engine: *mut boringssl::sys::ENGINE, output: &'a mut [u8], ) -> &'a mut [u8] { boringssl::ERR_clear_error(); @@ -268,7 +268,7 @@ impl EVP { /// # Safety /// `engine` must be either null or a valid `ENGINE` pointer. - pub fn copy(&self, engine: *mut boringssl::ENGINE) -> Result { + pub fn copy(&self, engine: *mut boringssl::sys::ENGINE) -> Result { boringssl::ERR_clear_error(); // SAFETY: self.md is a static singleton; caller upholds `engine`. let mut new = EVP::init(self.algorithm, self.md, engine); @@ -282,7 +282,7 @@ impl EVP { /// # Safety /// `engine` must be either null or a valid `ENGINE` pointer. - pub fn by_name_and_engine(engine: *mut boringssl::ENGINE, name: &[u8]) -> Option { + pub fn by_name_and_engine(engine: *mut boringssl::sys::ENGINE, name: &[u8]) -> Option { if let Some(algorithm) = lookup_ignore_case(name) { if let Some(md) = algorithm.md() { // `Algorithm::md()` lives in `bun_sha_hmac` @@ -308,17 +308,12 @@ impl EVP { pub fn by_name(name: &ZigString, global: &JSGlobalObject) -> Option { let name_str = name.to_slice(); - // `RareData::boring_engine()` returns `*mut` to bun_jsc's local opaque `ENGINE` - // stub (bun_jsc has no bun_boringssl_sys dep). Both name the same C `ENGINE` - // struct, so cast to the real bindgen type for the FFI call. + // `RareData::boring_engine()` borrows the VM-owned `ENGINE` out of the + // owning `bun_boringssl_sys::ENGINE` handle stored on `RareData`, so it + // already returns `*mut boringssl::sys::ENGINE` — no cast needed. // SAFETY: `bun_vm()` returns the raw `*mut VirtualMachine` for a Bun-owned // global (never null, single-threaded JS heap), so deref-to-&mut is sound here. - let engine = global - .bun_vm() - .as_mut() - .rare_data() - .boring_engine() - .cast::(); + let engine = global.bun_vm().as_mut().rare_data().boring_engine(); // SAFETY: `boring_engine()` returns the VM's lazily-initialized ENGINE (valid or null). Self::by_name_and_engine(engine, name_str.slice()) } diff --git a/src/runtime/dispatch.rs b/src/runtime/dispatch.rs index 2d2babb6665e..2a9b511f4572 100644 --- a/src/runtime/dispatch.rs +++ b/src/runtime/dispatch.rs @@ -125,7 +125,7 @@ use crate::napi::{NapiFinalizerTask, ThreadSafeFunction, napi_async_work}; use bun_jsc::PosixSignalTask; use bun_jsc::RuntimeTranspilerStore; -use bun_jsc::cpp_task::CppTask; +use bun_jsc::cpp_task::{self, CppTask}; use bun_jsc::hot_reloader; use bun_jsc::jsc_scheduler::JSCDeferredWorkTask; @@ -266,7 +266,15 @@ pub fn run_task( } } task_tag::CppTask => { - if let Err(err) = cast!(CppTask).run(global) { + // SAFETY: §Dispatch — tag identifies pointee and the pointer is a + // non-null `EventLoopTask*` the queue solely owns. `run` consumes + // it (`performTask` → `delete this`), on the error path too. + let task = unsafe { + CppTask::adopt(core::ptr::NonNull::new_unchecked(cast_ptr!( + cpp_task::sys::CppTask + ))) + }; + if let Err(err) = task.run(global) { report_error_or_terminate(global, err)?; } } diff --git a/src/runtime/dns_jsc/dns.rs b/src/runtime/dns_jsc/dns.rs index d6017fa9d52a..4bcf00efe0a0 100644 --- a/src/runtime/dns_jsc/dns.rs +++ b/src/runtime/dns_jsc/dns.rs @@ -4098,7 +4098,7 @@ impl Resolver { if self.any_requests_pending() { // SAFETY: `channel` is the live c-ares channel owned by `self`. c_ares::ares_process_fd( - unsafe { &mut *channel }, + unsafe { &*channel }, c_ares::ARES_SOCKET_BAD, c_ares::ARES_SOCKET_BAD, ); @@ -5671,7 +5671,7 @@ impl Resolver { { let ip = u32::from_be_bytes([addr[0], addr[1], addr[2], addr[3]]); // SAFETY: `channel` is a live handle returned by `ares_init_options`. - c_ares::ares_set_local_ip4(unsafe { &mut *channel }, ip); + c_ares::ares_set_local_ip4(unsafe { &*channel }, ip); return Ok(c_ares::AF::INET); } @@ -5888,7 +5888,7 @@ impl Resolver { ) -> JsResult { let channel = self.get_channel_or_error(global_this)?; // SAFETY: `channel` is a live handle returned by `ares_init_options`. - c_ares::ares_cancel(unsafe { &mut *channel }); + c_ares::ares_cancel(unsafe { &*channel }); Ok(JSValue::UNDEFINED) } diff --git a/src/runtime/ffi/ffi_body.rs b/src/runtime/ffi/ffi_body.rs index df41c132fc3e..cf61bda7e6ce 100644 --- a/src/runtime/ffi/ffi_body.rs +++ b/src/runtime/ffi/ffi_body.rs @@ -275,7 +275,7 @@ impl Source { pub(crate) fn add( &self, - state: &mut TCC::State, + state: &TCC::State, current_file_for_errors: &mut ZBox, ) -> Result<(), bun_core::Error> { match self { @@ -346,7 +346,7 @@ mod stdarg { static FFI_STDERRP: AtomicPtr; } - pub(super) fn inject(state: &mut TCC::State) { + pub(super) fn inject(state: &TCC::State) { // Taking addresses of process-global FILE* pointers; the statics // live for the process and we never form a Rust reference to them // (only a raw `*const c_void` for tcc_add_symbol). @@ -368,10 +368,10 @@ mod stdarg { #[cfg(not(target_os = "macos"))] mod mac { use super::*; - pub(super) fn inject(_: &mut TCC::State) {} + pub(super) fn inject(_: &TCC::State) {} } - pub(super) fn inject(state: &mut TCC::State) { + pub(super) fn inject(state: &TCC::State) { state .add_symbols(&[ // printf family @@ -645,9 +645,7 @@ impl CompileC { return Err(bun_core::err!("DeferredErrors")); } }; - // SAFETY: `state_ptr` was just returned non-null by `TCC::State::init`; - // we hold the only reference for the rest of this function. - let state: &mut TCC::State = unsafe { &mut *state_ptr.as_ptr() }; + let state: &TCC::State = TCC::State::opaque_ref(state_ptr.as_ptr()); if let Some(compiler_rt_dir) = CompilerRT::dir() { if state.add_sys_include_path(compiler_rt_dir).is_err() { @@ -2010,8 +2008,7 @@ impl Function { // SAFETY: we own the state unsafe { TCC::State::destroy(s.as_ptr()) }; }); - // SAFETY: `state_ptr` was just returned non-null by `TCC::State::init`. - let state: &mut TCC::State = unsafe { &mut *state_ptr.as_ptr() }; + let state: &TCC::State = TCC::State::opaque_ref(state_ptr.as_ptr()); if let Some(env) = napi_env { // `env` is the live VM-owned napi env; process-lifetime. @@ -2134,8 +2131,7 @@ impl Function { // SAFETY: we own the state unsafe { TCC::State::destroy(s.as_ptr()) }; }); - // SAFETY: `state_ptr` was just returned non-null by `TCC::State::init`. - let state: &mut TCC::State = unsafe { &mut *state_ptr.as_ptr() }; + let state: &TCC::State = TCC::State::opaque_ref(state_ptr.as_ptr()); if self.needs_napi_env() { if state @@ -2627,7 +2623,7 @@ impl CompilerRT { } } - pub(crate) fn define(state: &mut TCC::State) { + pub(crate) fn define(state: &TCC::State) { #[cfg(target_arch = "x86_64")] { state.define_symbol(zstr!("NEEDS_COMPILER_RT_FUNCTIONS"), zstr!("1")); @@ -2671,7 +2667,7 @@ impl CompilerRT { ]); } - pub(crate) fn inject(state: &mut TCC::State) { + pub(crate) fn inject(state: &TCC::State) { state .add_symbol(zstr!("memset"), Self::memset as *const c_void) .expect("unreachable"); diff --git a/src/runtime/image/codec_png.rs b/src/runtime/image/codec_png.rs index 18281eec3577..873afe874984 100644 --- a/src/runtime/image/codec_png.rs +++ b/src/runtime/image/codec_png.rs @@ -8,42 +8,90 @@ use super::codecs; use super::quantize; use crate::encoded_wrap_free; -bun_opaque::opaque_ffi! { pub struct spng_ctx; } +/// The C object itself. Only the extern declarations below name this type; +/// all Rust code uses the owning [`spng_ctx`] handle. +pub mod sys { + bun_opaque::opaque_ffi! { + /// libspng's `spng_ctx`. `&Self` is ABI-identical to a non-null + /// `spng_ctx *` and carries no `noalias`/`readonly` — libspng mutates + /// the context's parse/encode state through it on every call. + pub struct spng_ctx; + } +} unsafe extern "C" { - fn spng_ctx_new(flags: c_int) -> *mut spng_ctx; - fn spng_ctx_free(ctx: *mut spng_ctx); - fn spng_set_png_buffer(ctx: *mut spng_ctx, buf: *const u8, len: usize) -> c_int; - fn spng_decoded_image_size(ctx: *mut spng_ctx, fmt: c_int, out: *mut usize) -> c_int; + fn spng_ctx_new(flags: c_int) -> *mut sys::spng_ctx; + // NOT `safe fn`: this deallocates. A `safe fn` taking `&sys::spng_ctx` would + // let safe code free a context the handle still owns. Reached only through + // `spng_ctx_free_release`, below. + fn spng_ctx_free(ctx: *mut sys::spng_ctx); + fn spng_set_png_buffer(ctx: &sys::spng_ctx, buf: *const u8, len: usize) -> c_int; + fn spng_decoded_image_size(ctx: &sys::spng_ctx, fmt: c_int, out: *mut usize) -> c_int; fn spng_decode_image( - ctx: *mut spng_ctx, + ctx: &sys::spng_ctx, out: *mut u8, len: usize, fmt: c_int, flags: c_int, ) -> c_int; - fn spng_get_ihdr(ctx: *mut spng_ctx, ihdr: *mut Ihdr) -> c_int; - fn spng_set_ihdr(ctx: *mut spng_ctx, ihdr: *const Ihdr) -> c_int; - fn spng_set_plte(ctx: *mut spng_ctx, plte: *const Plte) -> c_int; - fn spng_set_trns(ctx: *mut spng_ctx, trns: *const Trns) -> c_int; + fn spng_get_ihdr(ctx: &sys::spng_ctx, ihdr: *mut Ihdr) -> c_int; + fn spng_set_ihdr(ctx: &sys::spng_ctx, ihdr: *const Ihdr) -> c_int; + fn spng_set_plte(ctx: &sys::spng_ctx, plte: *const Plte) -> c_int; + fn spng_set_trns(ctx: &sys::spng_ctx, trns: *const Trns) -> c_int; fn spng_encode_image( - ctx: *mut spng_ctx, + ctx: &sys::spng_ctx, img: *const u8, len: usize, fmt: c_int, flags: c_int, ) -> c_int; - fn spng_get_png_buffer(ctx: *mut spng_ctx, len: *mut usize, err: *mut c_int) -> *mut u8; - fn spng_set_option(ctx: *mut spng_ctx, opt: c_int, value: c_int) -> c_int; + fn spng_get_png_buffer(ctx: &sys::spng_ctx, len: *mut usize, err: *mut c_int) -> *mut u8; + // safe: the handle plus scalars; libspng only stores the option value. + safe fn spng_set_option(ctx: &sys::spng_ctx, opt: c_int, value: c_int) -> c_int; /// iCCP chunk read/write — PNG carries an optional ICC profile alongside /// the pixels for every colour type (including indexed). `spng_get_iccp` /// returns non-zero when the source has no iCCP (or the chunk was /// malformed); we treat all non-zero returns the same way — drop the /// profile — because the pixels are still valid and a PNG without iCCP /// is still a valid PNG. The `profile` pointer it hands back is owned by - /// the context and freed with `spng_ctx_free`; dupe out before then. - fn spng_get_iccp(ctx: *mut spng_ctx, iccp: *mut Iccp) -> c_int; - fn spng_set_iccp(ctx: *mut spng_ctx, iccp: *const Iccp) -> c_int; + /// the context and freed when the owning [`spng_ctx`] handle drops; dupe + /// out before then. + fn spng_get_iccp(ctx: &sys::spng_ctx, iccp: *mut Iccp) -> c_int; + fn spng_set_iccp(ctx: &sys::spng_ctx, iccp: *const Iccp) -> c_int; +} + +/// `ForeignOwned::release` hands us `&sys::spng_ctx`; libspng's destructor takes +/// `spng_ctx *`. `as_mut_ptr` is the sanctioned interior-mutability route to it. +fn spng_ctx_free_release(ctx: &sys::spng_ctx) { + // SAFETY: reached only from `ForeignRef::drop`, which owns the sole allocation + // `spng_ctx_new` returned and gives it back exactly once. + unsafe { spng_ctx_free(ctx.as_mut_ptr()) } +} + +// `spng_ctx_new` calloc's a fresh context and hands Rust the allocation. There +// is no refcount: `spng_ctx_free` unconditionally destroys the object, so one +// `spng_ctx` handle owns exactly that one allocation. +bun_opaque::foreign_handle! { + /// Owned handle to a libspng `spng_ctx`; `Drop` frees it. + /// + /// Every method takes `&self`: `sys::spng_ctx` is `UnsafeCell`-backed, so a + /// `&` carries no `noalias`/`readonly` and libspng mutates the context + /// through it on every call — including the ones C spells as taking a + /// non-const `spng_ctx *`. + pub struct spng_ctx(sys::spng_ctx) via spng_ctx_free_release; +} + +impl spng_ctx { + /// Allocate a context: `0` for a decoder, `SPNG_CTX_ENCODER` for an + /// encoder. `None` on allocation failure. + fn new(flags: c_int) -> Option { + // SAFETY: `spng_ctx_new` either returns null — for unrecognised flags, + // or because its `calloc` failed, in which case nothing was allocated + // and nothing needs freeing — or a fresh `calloc`'d context whose sole + // ownership unit it transfers to the caller. It frees nothing on any + // path, so no other handle can give this unit back. + unsafe { Self::adopt_ptr(spng_ctx_new(flags)) } + } } #[repr(C)] @@ -98,36 +146,28 @@ struct Trns { } pub fn decode(bytes: &[u8], max_pixels: u64) -> Result { - // SAFETY: spng_ctx_new is safe to call with any flags; null return = OOM. - let ctx = unsafe { spng_ctx_new(0) }; - if ctx.is_null() { - return Err(codecs::Error::OutOfMemory); - } - let _ctx_guard = scopeguard::guard(ctx, |c| { - // SAFETY: ctx was returned non-null by spng_ctx_new and is freed exactly once here. - unsafe { spng_ctx_free(c) } - }); + let ctx = spng_ctx::new(0).ok_or(codecs::Error::OutOfMemory)?; - // SAFETY: ctx is valid; bytes outlives the ctx (freed at end of scope). - if unsafe { spng_set_png_buffer(ctx, bytes.as_ptr(), bytes.len()) } != 0 { + // SAFETY: bytes outlives the ctx (dropped at end of scope). + if unsafe { spng_set_png_buffer(ctx.raw(), bytes.as_ptr(), bytes.len()) } != 0 { return Err(codecs::Error::DecodeFailed); } let mut ihdr = Ihdr::default(); - // SAFETY: ctx is valid; ihdr is a valid out-ptr. - if unsafe { spng_get_ihdr(ctx, &raw mut ihdr) } != 0 { + // SAFETY: ihdr is a valid out-ptr. + if unsafe { spng_get_ihdr(ctx.raw(), &raw mut ihdr) } != 0 { return Err(codecs::Error::DecodeFailed); } codecs::guard(ihdr.width, ihdr.height, max_pixels)?; let mut size: usize = 0; - // SAFETY: ctx is valid; size is a valid out-ptr. - if unsafe { spng_decoded_image_size(ctx, SPNG_FMT_RGBA8, &raw mut size) } != 0 { + // SAFETY: size is a valid out-ptr. + if unsafe { spng_decoded_image_size(ctx.raw(), SPNG_FMT_RGBA8, &raw mut size) } != 0 { return Err(codecs::Error::DecodeFailed); } let mut out = vec![0u8; size]; - // SAFETY: ctx is valid; out is a valid mutable buffer of `size` bytes. + // SAFETY: out is a valid mutable buffer of `size` bytes. if unsafe { spng_decode_image( - ctx, + ctx.raw(), out.as_mut_ptr(), out.len(), SPNG_FMT_RGBA8, @@ -141,15 +181,15 @@ pub fn decode(bytes: &[u8], max_pixels: u64) -> Result> = if unsafe { spng_get_iccp(ctx, &raw mut iccp) } == 0 + // SAFETY: iccp is a valid out-ptr. + let icc: Option> = if unsafe { spng_get_iccp(ctx.raw(), &raw mut iccp) } == 0 && iccp.profile_len > 0 && !iccp.profile.is_null() { @@ -177,7 +217,7 @@ pub fn decode(bytes: &[u8], max_pixels: u64) -> Result) { +fn embed_iccp(ctx: &spng_ctx, icc_profile: Option<&[u8]>) { let Some(p) = icc_profile else { return }; if p.is_empty() { return; @@ -192,8 +232,8 @@ fn embed_iccp(ctx: *mut spng_ctx, icc_profile: Option<&[u8]>) { }; let name = b"ICC Profile"; iccp.profile_name[..name.len()].copy_from_slice(name); - // SAFETY: ctx is valid; iccp is fully initialised; libspng only reads from it. - let _ = unsafe { spng_set_iccp(ctx, &raw const iccp) }; + // SAFETY: iccp is fully initialised; libspng only reads from it. + let _ = unsafe { spng_set_iccp(ctx.raw(), &raw const iccp) }; } pub(crate) fn encode( @@ -203,22 +243,15 @@ pub(crate) fn encode( level: i8, icc_profile: Option<&[u8]>, ) -> Result { - // SAFETY: spng_ctx_new is safe to call; null return = OOM. - let ctx = unsafe { spng_ctx_new(SPNG_CTX_ENCODER) }; - if ctx.is_null() { - return Err(codecs::Error::OutOfMemory); - } - let _ctx_guard = scopeguard::guard(ctx, |c| { - // SAFETY: ctx was returned non-null by spng_ctx_new and is freed exactly once here. - unsafe { spng_ctx_free(c) } - }); + let ctx = spng_ctx::new(SPNG_CTX_ENCODER).ok_or(codecs::Error::OutOfMemory)?; - // SAFETY: ctx is valid. - let _ = unsafe { spng_set_option(ctx, SPNG_ENCODE_TO_BUFFER, 1) }; + let _ = spng_set_option(ctx.raw(), SPNG_ENCODE_TO_BUFFER, 1); if level >= 0 { - // SAFETY: ctx is valid. - let _ = - unsafe { spng_set_option(ctx, SPNG_IMG_COMPRESSION_LEVEL, c_int::from(level.min(9))) }; + let _ = spng_set_option( + ctx.raw(), + SPNG_IMG_COMPRESSION_LEVEL, + c_int::from(level.min(9)), + ); } let ihdr = Ihdr { width: w, @@ -227,15 +260,15 @@ pub(crate) fn encode( color_type: SPNG_COLOR_TYPE_TRUECOLOR_ALPHA, ..Default::default() }; - // SAFETY: ctx is valid; ihdr is fully initialised. - if unsafe { spng_set_ihdr(ctx, &raw const ihdr) } != 0 { + // SAFETY: ihdr is fully initialised; libspng only reads from it. + if unsafe { spng_set_ihdr(ctx.raw(), &raw const ihdr) } != 0 { return Err(codecs::Error::EncodeFailed); } - embed_iccp(ctx, icc_profile); - // SAFETY: ctx is valid; rgba is a valid readable buffer. + embed_iccp(&ctx, icc_profile); + // SAFETY: rgba is a valid readable buffer. if unsafe { spng_encode_image( - ctx, + ctx.raw(), rgba.as_ptr(), rgba.len(), SPNG_FMT_PNG, @@ -247,8 +280,8 @@ pub(crate) fn encode( } let mut len: usize = 0; let mut err: c_int = 0; - // SAFETY: ctx is valid; len/err are valid out-ptrs. - let buf = unsafe { spng_get_png_buffer(ctx, &raw mut len, &raw mut err) }; + // SAFETY: len/err are valid out-ptrs. + let buf = unsafe { spng_get_png_buffer(ctx.raw(), &raw mut len, &raw mut err) }; if buf.is_null() { return Err(codecs::Error::EncodeFailed); } @@ -288,22 +321,15 @@ pub(crate) fn encode_indexed( ) .map_err(|_| codecs::Error::OutOfMemory)?; - // SAFETY: spng_ctx_new is safe to call; null return = OOM. - let ctx = unsafe { spng_ctx_new(SPNG_CTX_ENCODER) }; - if ctx.is_null() { - return Err(codecs::Error::OutOfMemory); - } - let _ctx_guard = scopeguard::guard(ctx, |c| { - // SAFETY: ctx was returned non-null by spng_ctx_new and is freed exactly once here. - unsafe { spng_ctx_free(c) } - }); + let ctx = spng_ctx::new(SPNG_CTX_ENCODER).ok_or(codecs::Error::OutOfMemory)?; - // SAFETY: ctx is valid. - let _ = unsafe { spng_set_option(ctx, SPNG_ENCODE_TO_BUFFER, 1) }; + let _ = spng_set_option(ctx.raw(), SPNG_ENCODE_TO_BUFFER, 1); if level >= 0 { - // SAFETY: ctx is valid. - let _ = - unsafe { spng_set_option(ctx, SPNG_IMG_COMPRESSION_LEVEL, c_int::from(level.min(9))) }; + let _ = spng_set_option( + ctx.raw(), + SPNG_IMG_COMPRESSION_LEVEL, + c_int::from(level.min(9)), + ); } let ihdr = Ihdr { @@ -313,11 +339,11 @@ pub(crate) fn encode_indexed( color_type: SPNG_COLOR_TYPE_INDEXED, ..Default::default() }; - // SAFETY: ctx is valid; ihdr is fully initialised. - if unsafe { spng_set_ihdr(ctx, &raw const ihdr) } != 0 { + // SAFETY: ihdr is fully initialised; libspng only reads from it. + if unsafe { spng_set_ihdr(ctx.raw(), &raw const ihdr) } != 0 { return Err(codecs::Error::EncodeFailed); } - embed_iccp(ctx, icc_profile); + embed_iccp(&ctx, icc_profile); let mut plte = Plte { n_entries: u32::from(q.colors), @@ -340,19 +366,19 @@ pub(crate) fn encode_indexed( ]; trns.type3_alpha[i] = q.palette[i * 4 + 3]; } - // SAFETY: ctx is valid; plte is fully initialised. - if unsafe { spng_set_plte(ctx, &raw const plte) } != 0 { + // SAFETY: plte is fully initialised; libspng only reads from it. + if unsafe { spng_set_plte(ctx.raw(), &raw const plte) } != 0 { return Err(codecs::Error::EncodeFailed); } - // SAFETY: ctx is valid; trns is fully initialised. - if q.has_alpha && unsafe { spng_set_trns(ctx, &raw const trns) } != 0 { + // SAFETY: trns is fully initialised; libspng only reads from it. + if q.has_alpha && unsafe { spng_set_trns(ctx.raw(), &raw const trns) } != 0 { return Err(codecs::Error::EncodeFailed); } - // SAFETY: ctx is valid; q.indices is a valid readable buffer. + // SAFETY: q.indices is a valid readable buffer. if unsafe { spng_encode_image( - ctx, + ctx.raw(), q.indices.as_ptr(), q.indices.len(), SPNG_FMT_PNG, @@ -365,8 +391,8 @@ pub(crate) fn encode_indexed( let mut len: usize = 0; let mut err: c_int = 0; - // SAFETY: ctx is valid; len/err are valid out-ptrs. - let buf = unsafe { spng_get_png_buffer(ctx, &raw mut len, &raw mut err) }; + // SAFETY: len/err are valid out-ptrs. + let buf = unsafe { spng_get_png_buffer(ctx.raw(), &raw mut len, &raw mut err) }; if buf.is_null() { return Err(codecs::Error::EncodeFailed); } diff --git a/src/runtime/image/codec_webp.rs b/src/runtime/image/codec_webp.rs index 7aab8d8901c1..7b3c30c2b0aa 100644 --- a/src/runtime/image/codec_webp.rs +++ b/src/runtime/image/codec_webp.rs @@ -82,9 +82,51 @@ struct WebPChunkIterator { private_: *mut c_void, } -bun_opaque::opaque_ffi! { - pub(crate) struct WebPDemuxer; - pub(crate) struct WebPMux; +/// The C objects themselves. Only the extern declarations below name these +/// types; all Rust code uses the owning [`WebPDemuxer`] / [`WebPMux`] handles. +pub(crate) mod sys { + bun_opaque::opaque_ffi! { + /// `struct WebPDemuxer` — libwebpdemux's parsed view of a RIFF file. + pub struct WebPDemuxer; + /// `struct WebPMux` — libwebpmux's in-progress RIFF container. + pub struct WebPMux; + } +} + +bun_opaque::foreign_handle! { + /// Owned handle to a libwebpdemux `WebPDemuxer`. + /// + /// `WebPDemuxInternal` hands back the sole owner of a freshly parsed + /// demuxer; `Drop` gives it back with `WebPDemuxDelete`. Not refcounted, so + /// "one ownership unit" is the object itself. + pub(crate) struct WebPDemuxer(sys::WebPDemuxer) via webp_demux_delete; +} + +bun_opaque::foreign_handle! { + /// Owned handle to a libwebpmux `WebPMux`. + /// + /// `WebPNewInternal` hands back the sole owner of a fresh, empty mux; `Drop` + /// gives it back with `WebPMuxDelete`. The assembled output of + /// [`WebPMuxAssemble`] is a separate `WebPMalloc` buffer that outlives the + /// mux, so dropping this handle does not invalidate it. + pub(crate) struct WebPMux(sys::WebPMux) via webp_mux_delete; +} + +/// `ForeignOwned::release` hands us `&sys::WebPDemuxer`; libwebp's destructor +/// takes `WebPDemuxer*`. `&` to an [`bun_opaque::opaque_ffi!`] ZST is +/// ABI-identical to that non-null pointer, and `as_mut_ptr` is the sanctioned +/// interior-mutability route to it. +fn webp_demux_delete(dmux: &sys::WebPDemuxer) { + // SAFETY: reached only from `ForeignRef::drop`, which owns the sole unit + // adopted from `WebPDemuxInternal` and gives it back exactly once. + unsafe { WebPDemuxDelete(dmux.as_mut_ptr()) } +} + +/// Same shape as [`webp_demux_delete`], for `WebPMuxDelete`. +fn webp_mux_delete(mux: &sys::WebPMux) { + // SAFETY: reached only from `ForeignRef::drop`, which owns the sole unit + // adopted from `WebPNewInternal` and gives it back exactly once. + unsafe { WebPMuxDelete(mux.as_mut_ptr()) } } // `WebPDemux()` and `WebPMuxNew()` are `static inline` in the headers and @@ -95,27 +137,31 @@ unsafe extern "C" { allow_partial: c_int, state: *mut c_int, version: c_int, - ) -> *mut WebPDemuxer; - fn WebPDemuxDelete(dmux: *mut WebPDemuxer); - fn WebPDemuxGetI(dmux: *const WebPDemuxer, feature: c_int) -> u32; + ) -> *mut sys::WebPDemuxer; + fn WebPDemuxDelete(dmux: *mut sys::WebPDemuxer); + fn WebPDemuxGetI(dmux: *const sys::WebPDemuxer, feature: c_int) -> u32; fn WebPDemuxGetChunk( - dmux: *const WebPDemuxer, + dmux: *const sys::WebPDemuxer, fourcc: *const u8, chunk_number: c_int, iter: *mut WebPChunkIterator, ) -> c_int; fn WebPDemuxReleaseChunkIterator(iter: *mut WebPChunkIterator); - fn WebPNewInternal(version: c_int) -> *mut WebPMux; - fn WebPMuxDelete(mux: *mut WebPMux); - fn WebPMuxSetImage(mux: *mut WebPMux, bitstream: *const WebPData, copy_data: c_int) -> c_int; + fn WebPNewInternal(version: c_int) -> *mut sys::WebPMux; + fn WebPMuxDelete(mux: *mut sys::WebPMux); + fn WebPMuxSetImage( + mux: *mut sys::WebPMux, + bitstream: *const WebPData, + copy_data: c_int, + ) -> c_int; fn WebPMuxSetChunk( - mux: *mut WebPMux, + mux: *mut sys::WebPMux, fourcc: *const u8, chunk_data: *const WebPData, copy_data: c_int, ) -> c_int; - fn WebPMuxAssemble(mux: *mut WebPMux, assembled_data: *mut WebPData) -> c_int; + fn WebPMuxAssemble(mux: *mut sys::WebPMux, assembled_data: *mut WebPData) -> c_int; } pub fn decode(bytes: &[u8], max_pixels: u64) -> Result { @@ -182,22 +228,23 @@ pub fn decode(bytes: &[u8], max_pixels: u64) -> Result::zeroed().assume_init() }; // SAFETY: dmux is live; fourcc reads exactly 4 bytes; iter is a valid out-param. - if unsafe { WebPDemuxGetChunk(dmux, b"ICCP".as_ptr(), 1, &raw mut iter) } == 0 { + if unsafe { WebPDemuxGetChunk(dmux.as_ptr(), b"ICCP".as_ptr(), 1, &raw mut iter) } == 0 { break 'blk None; } let iter = scopeguard::guard(iter, |mut it| { @@ -292,19 +339,20 @@ pub(crate) fn encode( }); // SAFETY: WebPNewInternal has no preconditions. let mux = unsafe { WebPNewInternal(WEBP_MUX_ABI_VERSION) }; - if mux.is_null() { + // SAFETY: `WebPNewInternal` is the producer: on success it transfers the + // sole ownership unit of a fresh, empty mux (`WebPMuxDelete` is its only + // deallocator). On ABI mismatch or allocation failure it returns null + // having allocated nothing, so `adopt_ptr` yields `None` and releases + // nothing. The handle's `Drop` deletes the mux on every path below. + let Some(mux) = (unsafe { WebPMux::adopt_ptr(mux) }) else { return Err(codecs::Error::OutOfMemory); - } - let _free_mux = scopeguard::guard(mux, |m| { - // SAFETY: m was returned by WebPNewInternal above and is non-null; matching destructor. - unsafe { WebPMuxDelete(m) } - }); + }; let img = WebPData { bytes: bitstream.as_ptr(), size: bitstream.len(), }; // SAFETY: mux is live; img points to valid borrowed data. - if unsafe { WebPMuxSetImage(mux, &raw const img, 0) } != WEBP_MUX_OK { + if unsafe { WebPMuxSetImage(mux.as_ptr(), &raw const img, 0) } != WEBP_MUX_OK { return Err(codecs::Error::EncodeFailed); } let icc = WebPData { @@ -312,12 +360,13 @@ pub(crate) fn encode( size: profile.len(), }; // SAFETY: mux is live; fourcc reads exactly 4 bytes; icc points to valid borrowed data. - if unsafe { WebPMuxSetChunk(mux, b"ICCP".as_ptr(), &raw const icc, 0) } != WEBP_MUX_OK { + if unsafe { WebPMuxSetChunk(mux.as_ptr(), b"ICCP".as_ptr(), &raw const icc, 0) } != WEBP_MUX_OK + { return Err(codecs::Error::EncodeFailed); } let mut assembled = WebPData::default(); // SAFETY: mux is live; assembled is a valid out-param. - if unsafe { WebPMuxAssemble(mux, &raw mut assembled) } != WEBP_MUX_OK { + if unsafe { WebPMuxAssemble(mux.as_ptr(), &raw mut assembled) } != WEBP_MUX_OK { // `WebPMuxAssemble` writes a half-built buffer into `assembled` even // on failure; its contract says `WebPDataClear` (i.e. `WebPFree`) is // safe to call on any return. diff --git a/src/runtime/ipc_host.rs b/src/runtime/ipc_host.rs index a01d7514dee2..f20b71c83560 100644 --- a/src/runtime/ipc_host.rs +++ b/src/runtime/ipc_host.rs @@ -144,9 +144,7 @@ pub(crate) fn do_send( match unsafe { (*listener).listener.get() } { crate::socket::listener::ListenerType::Uws(socket_uws) => { // may need to handle ssl case - let fd = bun_opaque::opaque_deref_mut(socket_uws) - .get_socket() - .get_fd(); + let fd = bun_opaque::opaque_deref(socket_uws).get_socket().get_fd(); zig_handle = Some(Handle::init(fd, handle)); } crate::socket::listener::ListenerType::NamedPipe(_named_pipe) => {} diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index 8c28462f487e..c2091c5eb131 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -1614,9 +1614,8 @@ unsafe fn retroactively_report_discovered_tests(agent: *mut bun_jsc::debugger::T let mut max_id: i32 = 0; // Recursively report all discovered tests starting from root scope. - // SAFETY: `agent` is a live C++ handle (fn contract). retroactively_report_scope( - unsafe { &mut *agent }, + TestReporterHandle::opaque_ref(agent), &mut active_file.collection.root_scope, -1, &mut max_id, @@ -1628,7 +1627,7 @@ unsafe fn retroactively_report_discovered_tests(agent: *mut bun_jsc::debugger::T let _ = max_id; fn retroactively_report_scope( - agent: &mut TestReporterHandle, + agent: &TestReporterHandle, scope: &mut DescribeScope, parent_id: i32, max_id: &mut i32, diff --git a/src/runtime/node/zlib/NativeZstd.rs b/src/runtime/node/zlib/NativeZstd.rs index 287f782dcc37..b6c4375fcdb6 100644 --- a/src/runtime/node/zlib/NativeZstd.rs +++ b/src/runtime/node/zlib/NativeZstd.rs @@ -442,10 +442,10 @@ mod _impl { self.flush as c_uint, ) }, - // SAFETY: state is a valid DCtx. + // SAFETY: state is a valid, non-null DCtx (checked above). NodeMode::ZSTD_DECOMPRESS => unsafe { c::ZSTD_decompressStream( - self.state_ptr().cast(), + c::ZSTD_DStream::opaque_ref(self.state_ptr().cast()), &raw mut self.output, &raw mut self.input, ) diff --git a/src/runtime/server/FileRoute.rs b/src/runtime/server/FileRoute.rs index bade2ef17de6..6171c82922c4 100644 --- a/src/runtime/server/FileRoute.rs +++ b/src/runtime/server/FileRoute.rs @@ -244,7 +244,7 @@ impl FileRoute { } } AnyResponse::H3(s) => { - let s = bun_opaque::opaque_deref_mut(s); + let s = bun_opaque::opaque_deref(s); for (name, value) in names.iter().zip(values) { s.write_header(sp_slice(*name, buf), sp_slice(*value, buf)); } @@ -275,7 +275,7 @@ impl FileRoute { let mut b = bun_core::fmt::ItoaBuf::new(); let s = bun_core::fmt::itoa(&mut b, status); // S008: `h3::Response` is an `opaque_ffi!` ZST — safe deref. - bun_opaque::opaque_deref_mut(r).write_status(s); + bun_opaque::opaque_deref(r).write_status(s); } } } diff --git a/src/runtime/server/RequestContext.rs b/src/runtime/server/RequestContext.rs index 06b810397799..ebf96c6f0010 100644 --- a/src/runtime/server/RequestContext.rs +++ b/src/runtime/server/RequestContext.rs @@ -76,7 +76,7 @@ impl AnyResponseExt for uws::AnyResponse { match self { uws::AnyResponse::SSL(p) => bun_opaque::opaque_deref_mut(p).has_responded(), uws::AnyResponse::TCP(p) => bun_opaque::opaque_deref_mut(p).has_responded(), - uws::AnyResponse::H3(p) => bun_opaque::opaque_deref_mut(p).has_responded(), + uws::AnyResponse::H3(p) => bun_opaque::opaque_deref(p).has_responded(), } } #[inline] @@ -88,9 +88,7 @@ impl AnyResponseExt for uws::AnyResponse { uws::AnyResponse::TCP(p) => { bun_opaque::opaque_deref_mut(p).override_write_offset(offset) } - uws::AnyResponse::H3(p) => { - bun_opaque::opaque_deref_mut(p).override_write_offset(offset) - } + uws::AnyResponse::H3(p) => bun_opaque::opaque_deref(p).override_write_offset(offset), } } } @@ -3634,7 +3632,7 @@ where self.do_write_status(status); } - if let Some(mut cookies) = self.cookies.take() { + if let Some(cookies) = self.cookies.take() { // SAFETY: BACKREF let global_this = self.server().global_this(); let r = cookies.write( diff --git a/src/runtime/server/ServerConfig.rs b/src/runtime/server/ServerConfig.rs index f27a88342d32..aecc6cebeaf2 100644 --- a/src/runtime/server/ServerConfig.rs +++ b/src/runtime/server/ServerConfig.rs @@ -325,7 +325,7 @@ impl ServerConfig { #[allow(clippy::not_unsafe_ptr_arg_deref)] pub(crate) fn apply_static_route( server: AnyServer, - app: &mut uws::NewApp, + app: &uws::NewApp, entry: *mut T, path: &[u8], method: http_method::Optional, diff --git a/src/runtime/server/StaticRoute.rs b/src/runtime/server/StaticRoute.rs index 8615fff46b54..8ef4336a7fe6 100644 --- a/src/runtime/server/StaticRoute.rs +++ b/src/runtime/server/StaticRoute.rs @@ -476,7 +476,7 @@ impl StaticRoute { let mut b = bun_core::fmt::ItoaBuf::new(); let s = bun_core::fmt::itoa(&mut b, status); // S008: `h3::Response` is an `opaque_ffi!` ZST — safe deref. - bun_opaque::opaque_deref_mut(r).write_status(s); + bun_opaque::opaque_deref(r).write_status(s); } } } diff --git a/src/runtime/server/mod.rs b/src/runtime/server/mod.rs index caa8b17cadab..f694a35428c4 100644 --- a/src/runtime/server/mod.rs +++ b/src/runtime/server/mod.rs @@ -534,7 +534,7 @@ impl NewServer { let mut port = *port; if let Some(listener) = self.listener { // S012: `app::ListenSocket` is a ZST opaque — safe deref. - port = bun_opaque::opaque_deref_mut(listener).get_local_port() as u16; + port = bun_opaque::opaque_deref(listener).get_local_port() as u16; } else if Self::HAS_H3 { if let Some(h3l) = self.h3_listener { // S012: `h3::ListenSocket` is an `opaque_ffi!` ZST — safe deref. @@ -1465,16 +1465,16 @@ impl NewServer { pub fn set_flags(&self, require_host_header: bool, use_strict_method_validation: bool) { if let Some(app) = self.app { - // S012: `NewApp` is a ZST opaque — safe `*mut → &mut` deref. - bun_opaque::opaque_deref_mut(app) + // S012: `NewApp` is a ZST opaque — safe `*mut → &` deref. + bun_opaque::opaque_deref(app) .set_flags(require_host_header, use_strict_method_validation); } } pub fn set_max_http_header_size(&self, max_header_size: u64) { if let Some(app) = self.app { - // S012: `NewApp` is a ZST opaque — safe `*mut → &mut` deref. - bun_opaque::opaque_deref_mut(app).set_max_http_header_size(max_header_size); + // S012: `NewApp` is a ZST opaque — safe `*mut → &` deref. + bun_opaque::opaque_deref(app).set_max_http_header_size(max_header_size); } } @@ -1554,14 +1554,14 @@ impl NewServer { if !abrupt { // S012: `app::ListenSocket` is a ZST opaque — safe deref. - bun_opaque::opaque_deref_mut(listener).close(); + bun_opaque::opaque_deref(listener).close(); } else if !self.flags.contains(ServerFlags::TERMINATED) { if let Some(ws) = self.config.websocket.as_mut() { ws.handler.app = None; } self.flags.insert(ServerFlags::TERMINATED); - // S012: `NewApp` is a ZST opaque — safe `*mut → &mut` deref. - bun_opaque::opaque_deref_mut(self.app.unwrap()).close(); + // S012: `NewApp` is a ZST opaque — safe `*mut → &` deref. + bun_opaque::opaque_deref(self.app.unwrap()).close(); } } @@ -1660,8 +1660,8 @@ impl NewServer { // binding alive should not pin `dev.memory_cost()` bytes. if let Some(dev) = self.dev_server.take() { if let Some(app) = self.app { - // S012: `NewApp` is a ZST opaque — safe `*mut → &mut` deref. - bun_opaque::opaque_deref_mut(app).clear_routes(); + // S012: `NewApp` is a ZST opaque — safe `*mut → &` deref. + bun_opaque::opaque_deref(app).clear_routes(); } drop(dev); // dev.deinit() } @@ -1690,8 +1690,8 @@ impl NewServer { self.flags.insert(ServerFlags::TERMINATED); let app = self.app.unwrap(); vm.enqueue_task(bun_event_loop::ManagedTask::ManagedTask::new(app, |app| { - // S008: `NewApp` is a ZST opaque — safe `*mut → &mut` deref. - bun_opaque::opaque_deref_mut(app).close(); + // S008: `NewApp` is a ZST opaque — safe `*mut → &` deref. + bun_opaque::opaque_deref(app).close(); Ok(()) })); } @@ -1980,9 +1980,9 @@ impl NewServer { fn set_routes(&mut self) -> JSValue { use bun_http_types::Method as http_method; let mut route_list_value = JSValue::ZERO; - // S008: `NewApp` is a ZST opaque — safe `*mut → &mut` deref. + // S008: `NewApp` is a ZST opaque — safe `*mut → &` deref. // set_routes is only called after `self.app = Some(..)` in listen(). - let app = bun_opaque::opaque_deref_mut(self.app.unwrap()); + let app = bun_opaque::opaque_deref(self.app.unwrap()); let self_ptr: *mut Self = self; let any_server = AnyServer::from(self_ptr.cast_const()); // reshaped for borrowck — `dev_server` is `Option>`; @@ -2038,7 +2038,7 @@ impl NewServer { if let Some(websocket) = self.config.websocket.as_mut() { websocket.global_object = bun_ptr::BackRef::new(bun_opaque::opaque_deref(self.global_this)); - websocket.handler.app = Some(std::ptr::from_mut(app).cast::()); + websocket.handler.app = Some(app.as_mut_ptr().cast::()); websocket .handler .flags @@ -2437,7 +2437,7 @@ impl NewServer { // compatibility layer for specific Node API routes, even if it's not // the main "/*" handler. if has_node_http { - ffi::NodeHTTP_assignOnNodeJSCompat(SSL, std::ptr::from_mut(app).cast::()); + ffi::NodeHTTP_assignOnNodeJSCompat(SSL, app.as_mut_ptr().cast::()); } route_list_value @@ -2551,8 +2551,8 @@ impl NewServer { // SAFETY: name_ptr/name_len were just extracted from the live // `config.ssl_config.server_name` CString; valid + NUL-terminated. let server_name = unsafe { bun_core::ffi::cstr(name_ptr) }; - // S012: `NewApp` is a ZST opaque — safe `*mut → &mut` deref. - if bun_opaque::opaque_deref_mut(app) + // S012: `NewApp` is a ZST opaque — safe `*mut → &` deref. + if bun_opaque::opaque_deref(app) .add_server_name_with_options(server_name, &ssl_options) .is_err() { @@ -2574,8 +2574,8 @@ impl NewServer { // SAFETY: server_name is a CStr; ZStr::from_raw upholds the NUL invariant. let z = unsafe { bun_core::ZStr::from_raw(name_ptr.cast(), name_len) }; - // S012: `NewApp` is a ZST opaque — safe `*mut → &mut` deref. - bun_opaque::opaque_deref_mut(app).domain(z); + // S012: `NewApp` is a ZST opaque — safe `*mut → &` deref. + bun_opaque::opaque_deref(app).domain(z); if throw_ssl_error_if_necessary(global) { // SAFETY: `this` is the live boxed server from `init()`, uniquely owned here. Self::deinit(unsafe { bun_core::heap::take(this) }); @@ -2633,8 +2633,8 @@ impl NewServer { } } } - // S012: `NewApp` is a ZST opaque — safe `*mut → &mut` deref. - if bun_opaque::opaque_deref_mut(app) + // S012: `NewApp` is a ZST opaque — safe `*mut → &` deref. + if bun_opaque::opaque_deref(app) .add_server_name_with_options(sni_name, &sni_opts) .is_err() { @@ -2648,8 +2648,8 @@ impl NewServer { Self::deinit(unsafe { bun_core::heap::take(this) }); return JSValue::ZERO; } - // S012: `NewApp` is a ZST opaque — safe `*mut → &mut` deref. - bun_opaque::opaque_deref_mut(app).domain(z); + // S012: `NewApp` is a ZST opaque — safe `*mut → &` deref. + bun_opaque::opaque_deref(app).domain(z); if throw_ssl_error_if_necessary(global) { // SAFETY: `this` is the live boxed server from `init()`, uniquely owned here. Self::deinit(unsafe { bun_core::heap::take(this) }); @@ -3517,7 +3517,7 @@ impl AnyServer { any_server_dispatch!(self, |s| match s.app { // S012: `NewApp` is a ZST opaque — safe `*mut → &mut` via // `bun_opaque::opaque_deref_mut` (const-asserted ZST/align-1). - Some(app) => bun_opaque::opaque_deref_mut(app).num_subscribers(topic), + Some(app) => bun_opaque::opaque_deref(app).num_subscribers(topic), // Defensive 0 // here for the post-stop window; assert in debug to catch misuse. None => { @@ -3537,8 +3537,7 @@ impl AnyServer { any_server_dispatch!(self, |s| match s.app { // S012: `NewApp` is a ZST opaque — safe `*mut → &mut` via // `bun_opaque::opaque_deref_mut` (const-asserted ZST/align-1). - Some(app) => - bun_opaque::opaque_deref_mut(app).publish(topic, message, opcode, compress), + Some(app) => bun_opaque::opaque_deref(app).publish(topic, message, opcode, compress), // Defensive for the post-stop window; assert in debug to catch misuse. None => { debug_assert!(false, "publish on server with no app"); diff --git a/src/runtime/server/server_body.rs b/src/runtime/server/server_body.rs index 7ca2ac160970..e7ee3fd28033 100644 --- a/src/runtime/server/server_body.rs +++ b/src/runtime/server/server_body.rs @@ -1343,15 +1343,15 @@ impl NewServer { GlobalRef::from(bun_opaque::opaque_deref(self.global_this)) } - /// `&mut` accessor for the live uws App. Only call from paths where the + /// `&` accessor for the live uws App. Only call from paths where the /// server is running (`self.app` set in `listen()`). #[inline] - fn app_mut(&mut self) -> &mut uws_sys::NewApp { - // S008: `NewApp` is a ZST opaque — safe `*mut → &mut` deref via - // const-asserted `bun_opaque::opaque_deref_mut`. `self.app` is `Some` + fn app_ref(&self) -> &uws_sys::NewApp { + // S008: `NewApp` is a ZST opaque — safe `*mut → &` deref via + // const-asserted `bun_opaque::opaque_deref`. `self.app` is `Some` // for the lifetime of any JS-reachable `Server` (set in `listen()`, // freed in `deinit()` after the JS wrapper is gone). - bun_opaque::opaque_deref_mut(self.app.expect("server not listening")) + bun_opaque::opaque_deref(self.app.expect("server not listening")) } /// Unbounded so `deinit()` (in @@ -1463,7 +1463,7 @@ where } Ok(JSValue::js_number(f64::from( - self.app_mut().num_subscribers(topic.slice()), + self.app_ref().num_subscribers(topic.slice()), ))) } @@ -1938,7 +1938,7 @@ where // `uws_sys::Request` here. // S008: `uws::Request` is an `opaque_ffi!` ZST — safe deref // (BACKREF; live while RequestContext.req is Some). - let r = bun_opaque::opaque_deref_mut(req_ptr.cast::()); + let r = bun_opaque::opaque_deref(req_ptr.cast::()); if sec_websocket_key_str.len == 0 { sec_websocket_key_str = ZigString::init(r.header(b"sec-websocket-key").unwrap_or(b"")); @@ -2051,7 +2051,7 @@ where // `CookieMapRef` releases the moved-out ref on every exit path of this // scope (including the `?` below) once `cookies_to_write` drops. - let mut cookies_to_write = upgrader.cookies.take(); + let cookies_to_write = upgrader.cookies.take(); // Write status, custom headers, and cookies in one place if fetch_headers_to_use.is_some() || cookies_to_write.is_some() { @@ -2066,7 +2066,7 @@ where ); } } - if let Some(c) = cookies_to_write.as_mut() { + if let Some(c) = cookies_to_write.as_ref() { c.write( global, ResponseKind::from(SSL, false), @@ -2128,7 +2128,7 @@ where // SAFETY: `on_reload` is only reachable while the server is running // (`self.app` set in `listen()`). - self.app_mut().clear_routes(); + self.app_ref().clear_routes(); if Self::HAS_H3 { if let Some(h3a) = self.h3_app { bun_opaque::opaque_deref_mut(h3a).clear_routes(); @@ -2222,7 +2222,7 @@ where return Ok(false); } self.config = self.config.clone_for_reloading_static_routes()?; - self.app_mut().clear_routes(); + self.app_ref().clear_routes(); if Self::HAS_H3 { if let Some(h3a) = self.h3_app { bun_opaque::opaque_deref_mut(h3a).clear_routes(); @@ -2452,7 +2452,7 @@ where if self.app.is_none() { return Ok(JSValue::UNDEFINED); } - self.app_mut().close_idle_connections(); + self.app_ref().close_idle_connections(); Ok(JSValue::UNDEFINED) } @@ -2489,9 +2489,7 @@ where if let Some(listener) = self.listener { // S008: `app::ListenSocket` is a ZST opaque — safe deref. - return JSValue::js_number( - bun_opaque::opaque_deref_mut(listener).get_local_port() as f64 - ); + return JSValue::js_number(bun_opaque::opaque_deref(listener).get_local_port() as f64); } if Self::HAS_H3 { if let Some(h3l) = self.h3_listener { @@ -2537,7 +2535,7 @@ where if let Some(listener) = self.listener { // S008: `app::ListenSocket` is a ZST opaque — safe deref. - let listener = bun_opaque::opaque_deref_mut(listener); + let listener = bun_opaque::opaque_deref(listener); port = u16::try_from(listener.get_local_port()).expect("int cast"); let mut buf = [0u8; 64]; @@ -2600,7 +2598,7 @@ where if let Some(listener) = self.listener { let mut buf = [0u8; 1024]; // S008: `app::ListenSocket` is a ZST opaque — safe deref. - if let Some(addr) = bun_opaque::opaque_deref_mut(listener) + if let Some(addr) = bun_opaque::opaque_deref(listener) .socket() .remote_address(&mut buf[..1024]) { @@ -3618,8 +3616,8 @@ pub(super) fn server_set_on_client_error_( // S008: `us_socket_t` is an `opaque_ffi!` ZST — safe deref. this.on_client_error_callback(bun_opaque::opaque_deref_mut(socket), error_code, packet); } - // S008: `NewApp` is a ZST opaque — safe `*mut → &mut` deref. - bun_opaque::opaque_deref_mut(app).on_client_error(thunk, core::ptr::from_ref::<$T>(this).cast::().cast_mut()); + // S008: `NewApp` is a ZST opaque — safe `*mut → &` deref. + bun_opaque::opaque_deref(app).on_client_error(thunk, core::ptr::from_ref::<$T>(this).cast::().cast_mut()); } return Ok(JSValue::UNDEFINED); } diff --git a/src/runtime/socket/Listener.rs b/src/runtime/socket/Listener.rs index 0afc90b2d9db..f70c3d1ee13b 100644 --- a/src/runtime/socket/Listener.rs +++ b/src/runtime/socket/Listener.rs @@ -85,7 +85,7 @@ pub struct Listener { /// `SSL_CTX*` for accepted sockets. One owned ref; `SSL_CTX_free` on close. /// `SSL_new()` per-accept takes its own ref, so accepted sockets outlive a /// stopped listener safely. - pub secure_ctx: Cell>>, + pub secure_ctx: Cell>>, pub ssl: bool, pub protos: Option>, @@ -335,8 +335,8 @@ impl Listener { // SAFETY: this is still the sole owner on the error path let this_owned: Box = unsafe { bun_core::heap::take(this) }; if let Some(c) = this_owned.secure_ctx.get() { - // SAFETY: FFI — secure_ctx holds one owned SSL_CTX ref from create_ssl_context - unsafe { boring_sys::SSL_CTX_free(c.as_ptr()) }; + // secure_ctx holds one owned SSL_CTX ref from create_ssl_context. + boring_sys::SSL_CTX_free(boring_sys::sys::SSL_CTX::opaque_ref(c.as_ptr())); } // protos: Box drops automatically when Listener is dropped below bun_core::asan::unregister_root_region( @@ -353,7 +353,7 @@ impl Listener { match ssl_cfg.as_usockets().create_ssl_context(&mut create_err) { Some(ctx) => this_ref .secure_ctx - .set(NonNull::new(ctx.cast::())), + .set(NonNull::new(ctx.cast::())), None => { return Err(global.throw_value( crate::socket::uws_jsc::create_bun_socket_error_to_js(create_err, global), @@ -403,7 +403,7 @@ impl Listener { }); if !ls.is_null() { // S008: `ListenSocket` is an `opaque_ffi!` ZST — safe deref. - *port = u16::try_from(bun_opaque::opaque_deref_mut(ls).get_local_port()) + *port = u16::try_from(bun_opaque::opaque_deref(ls).get_local_port()) .expect("int cast"); } ls @@ -496,7 +496,7 @@ impl Listener { // hint for sni_cb, not load-bearing — sni_find() miss falls // through to the default SSL_CTX anyway. // S008: `ListenSocket` is an `opaque_ffi!` ZST — safe deref. - let _ = bun_opaque::opaque_deref_mut(listen_socket).add_server_name( + let _ = bun_opaque::opaque_deref(listen_socket).add_server_name( server_name, secure.as_ptr().cast(), core::ptr::null_mut(), @@ -513,7 +513,7 @@ impl Listener { // resolution suspends the handshake until resumeSNI. if !this_ref.handlers.on_server_name().is_empty() { // S008: `ListenSocket` is an `opaque_ffi!` ZST - safe deref. - bun_opaque::opaque_deref_mut(listen_socket).on_server_name(us_dispatch_server_name); + bun_opaque::opaque_deref(listen_socket).on_server_name(us_dispatch_server_name); } } @@ -613,7 +613,7 @@ impl Listener { } if let uws::InternalSocket::Connected(s) = socket.socket { // S008: `us_socket_t` is an `opaque_ffi!` ZST — safe deref. - bun_opaque::opaque_deref_mut(s).set_kind(if SSL { + bun_opaque::opaque_deref(s).set_kind(if SSL { uws_sys::SocketKind::BunSocketTls } else { uws_sys::SocketKind::BunSocketTcp @@ -661,7 +661,7 @@ impl Listener { // node:tls passes the native SecureContext (already-built SSL_CTX*) — no // re-parse. Bun.listen({tls}) callers may still pass a raw options dict. - let sni_ctx: *mut boring_sys::SSL_CTX = + let sni_ctx: *mut boring_sys::sys::SSL_CTX = if let Some(sc) = tls.as_class_ref::() { sc.borrow() } else if let Some(ssl_config) = { @@ -693,11 +693,11 @@ impl Listener { // The C SNI tree SSL_CTX_up_ref()s; drop our build/borrow ref once added. // S008: `ListenSocket` is an `opaque_ffi!` ZST — safe deref. - let ls_ref = bun_opaque::opaque_deref_mut(ls); + let ls_ref = bun_opaque::opaque_deref(ls); ls_ref.remove_server_name(server_name); let ok = ls_ref.add_server_name(server_name, sni_ctx.cast(), core::ptr::null_mut()); - // SAFETY: FFI — drop the +1 ref we took via borrow()/get_or_create(); SNI tree up_ref'd its own - unsafe { boring_sys::SSL_CTX_free(sni_ctx) }; + // Drop the +1 we took via borrow()/get_or_create(); the SNI tree up_ref'd its own. + boring_sys::SSL_CTX_free(boring_sys::sys::SSL_CTX::opaque_ref(sni_ctx)); if !ok { // Old entry was already removed; failing silently would leave the // hostname with no SNI mapping at all. Surface it. @@ -761,7 +761,7 @@ impl Listener { match listener { // S008: `ListenSocket` is an `opaque_ffi!` ZST — safe deref. - ListenerType::Uws(socket) => bun_opaque::opaque_deref_mut(socket).close(), + ListenerType::Uws(socket) => bun_opaque::opaque_deref(socket).close(), #[cfg(windows)] ListenerType::NamedPipe(named_pipe) => { // SAFETY: named_pipe is the unique owner; close_pipe_and_deinit @@ -783,7 +783,7 @@ impl Listener { ListenerType::Uws(socket) => { Self::unlink_unix_socket_path(&self); // S008: `ListenSocket` is an `opaque_ffi!` ZST — safe deref. - bun_opaque::opaque_deref_mut(socket).close(); + bun_opaque::opaque_deref(socket).close(); } #[cfg(windows)] ListenerType::NamedPipe(named_pipe) => { @@ -837,8 +837,8 @@ impl Listener { // SAFETY: group was init'd in listen(); not concurrently walked. unsafe { uws::SocketGroup::destroy(self.group.as_ptr()) }; if let Some(ctx) = self.secure_ctx.get() { - // SAFETY: FFI — secure_ctx holds one owned SSL_CTX ref; release it - unsafe { boring_sys::SSL_CTX_free(ctx.as_ptr()) }; + // secure_ctx holds one owned SSL_CTX ref; release it. + boring_sys::SSL_CTX_free(boring_sys::sys::SSL_CTX::opaque_ref(ctx.as_ptr())); } // connection / protos / the handlers `Rc`: dropped with the Box below @@ -878,7 +878,7 @@ impl Listener { match this.listener.get() { ListenerType::Uws(uws_listener) => { // S008: `ListenSocket` is an `opaque_ffi!` ZST — safe deref. - let socket = bun_opaque::opaque_deref_mut(uws_listener).socket::(); + let socket = bun_opaque::opaque_deref(uws_listener).socket::(); // On Windows the listening socket fd is a system-kind SOCKET // handle; routing it through `.uv()` panics for anything but // stdio. The sys_jsc helper branches on kind @@ -995,7 +995,7 @@ impl Listener { // Resolve the prebuilt SSL_CTX before the platform branches so the Windows // named-pipe path can adopt it. node:tls passes the native SecureContext as // `tls.secureContext` so we share its already-built SSL_CTX. - let mut owned_ssl_ctx: Option> = None; + let mut owned_ssl_ctx: Option> = None; if ssl_enabled { let native_sc: Option<&SecureContext> = 'blk: { let Some(tls_js) = opts.get_truthy(global, "tls")? else { @@ -1015,8 +1015,8 @@ impl Listener { } let mut ssl_ctx_guard = scopeguard::guard(owned_ssl_ctx, |c| { if let Some(c) = c { - // SAFETY: FFI — c is a live SSL_CTX* with one owned ref from borrow()/get_or_create() - unsafe { boring_sys::SSL_CTX_free(c.as_ptr()) }; + // One owned ref from borrow()/get_or_create(). + boring_sys::SSL_CTX_free(boring_sys::sys::SSL_CTX::opaque_ref(c.as_ptr())); } }); @@ -1249,7 +1249,7 @@ impl Listener { let mut create_err = uws::create_bun_socket_error_t::none; match with_ssl_ctx_cache(|cache| cache.get_or_create(ssl_cfg, &mut create_err)) { Some(ctx) => { - *ssl_ctx_guard = NonNull::new(ctx.cast::()); + *ssl_ctx_guard = NonNull::new(ctx.cast::()); } None => { return Err(global.throw_value( @@ -1327,7 +1327,7 @@ impl Listener { let mut buf = [0u8; 64]; let mut text_buf = [0u8; 512]; // S008: `ListenSocket` is an `opaque_ffi!` ZST — safe deref. - let socket_ref = bun_opaque::opaque_deref_mut(socket); + let socket_ref = bun_opaque::opaque_deref(socket); let address_bytes: &[u8] = match socket_ref.get_local_address(&mut buf) { Ok(b) => b, Err(_) => return Ok(JSValue::UNDEFINED), @@ -1378,7 +1378,7 @@ fn connect_finish( connection: UnixOrHost, local_binding: Option<(Box<[u8]>, u16)>, mut ssl: Option<&mut SSLConfig>, - owned_ssl_ctx: Option>, + owned_ssl_ctx: Option>, default_data: JSValue, allow_half_open: bool, port: Option, @@ -1409,8 +1409,8 @@ fn connect_finish( prev.server_name .set(ssl.as_mut().and_then(|s| s.take_server_name())); if let Some(old) = prev.owned_ssl_ctx.get() { - // SAFETY: FFI — old is the previous owned SSL_CTX ref on this reused socket - unsafe { boring_sys::SSL_CTX_free(old) }; + // `old` is the previous owned SSL_CTX ref on this reused socket. + boring_sys::SSL_CTX_free(boring_sys::sys::SSL_CTX::opaque_ref(old)); } prev.owned_ssl_ctx.set(owned_ssl_ctx.map(|p| p.as_ptr())); prev @@ -1567,7 +1567,7 @@ pub struct WindowsNamedPipeListeningContext { /// JSC_BORROW: process-lifetime singleton; `&'static` so call sites read /// `self.vm.is_shutting_down()` without a raw-pointer deref. pub vm: &'static VirtualMachine, - pub ctx: Option>, // server reuses the same ctx + pub ctx: Option>, // server reuses the same ctx } #[cfg(not(windows))] @@ -1692,7 +1692,7 @@ impl WindowsNamedPipeListeningContext { let mut err = uws::create_bun_socket_error_t::none; // Create SSL context using uSockets to match behavior of node.js match ctx_opts.create_ssl_context(&mut err) { - Some(ctx) => this_ref.ctx = NonNull::new(ctx.cast::()), + Some(ctx) => this_ref.ctx = NonNull::new(ctx.cast::()), None => return Err(bun_core::err!("InvalidOptions")), } } @@ -1740,8 +1740,8 @@ impl WindowsNamedPipeListeningContext { fn deinit(mut self: Box) { self.listener = None; if let Some(ctx) = self.ctx.take() { - // SAFETY: the server owns the only reference to this context. - unsafe { boring_sys::SSL_CTX_free(ctx.as_ptr()) }; + // The server owns the only reference to this context. + boring_sys::SSL_CTX_free(boring_sys::sys::SSL_CTX::opaque_ref(ctx.as_ptr())); } } } @@ -1775,7 +1775,7 @@ pub(crate) extern "C" fn us_dispatch_server_name( } // The accept group's ext holds the owning `*mut Listener` for the lifetime // of the listen socket. S008: `ListenSocket` is an `opaque_ffi!` ZST. - let listener_ptr: *mut Listener = bun_opaque::opaque_deref_mut(ls).group().owner::(); + let listener_ptr: *mut Listener = bun_opaque::opaque_deref(ls).group().owner::(); if listener_ptr.is_null() { return core::ptr::null_mut(); } diff --git a/src/runtime/socket/SSLConfig.rs b/src/runtime/socket/SSLConfig.rs index 919a47068e10..a7b54a3b5bb3 100644 --- a/src/runtime/socket/SSLConfig.rs +++ b/src/runtime/socket/SSLConfig.rs @@ -306,8 +306,8 @@ fn handle_file( jsc::generated::SSLConfigFile::String(val) => SingleFile::String(val.get()), jsc::generated::SSLConfigFile::Buffer(val) => { // SAFETY: GenVal::get() yields a non-null pointer valid for the - // lifetime of `generated`; we narrow it to `&mut` for the call. - SingleFile::Buffer(unsafe { &mut *val.get() }) + // lifetime of `generated`. + SingleFile::Buffer(unsafe { &*val.get() }) } jsc::generated::SSLConfigFile::File(val) => { // SAFETY: opaque `GenBlob` (`*mut c_void`) is the JS class `m_ctx` @@ -348,7 +348,7 @@ fn handle_file_array( jsc::generated::SSLConfigSingleFile::Buffer(val) => { // SAFETY: see `handle_file` above — non-null GenVal pointers // valid for the lifetime of `generated`. - SingleFile::Buffer(unsafe { &mut *val.get() }) + SingleFile::Buffer(unsafe { &*val.get() }) } jsc::generated::SSLConfigSingleFile::File(val) => { // SAFETY: opaque `GenBlob` (`*mut c_void`) is layout-identical @@ -364,7 +364,7 @@ fn handle_file_array( enum SingleFile<'a> { String(bun_core::String), - Buffer(&'a mut jsc::JSCArrayBuffer), + Buffer(&'a jsc::JSCArrayBuffer), File(&'a mut crate::webcore::Blob), } diff --git a/src/runtime/socket/SocketAddress.rs b/src/runtime/socket/SocketAddress.rs index 39224146fc91..bba9d30d0acc 100644 --- a/src/runtime/socket/SocketAddress.rs +++ b/src/runtime/socket/SocketAddress.rs @@ -209,15 +209,10 @@ impl SocketAddress { OwnedString::new(str) }; - let Some(url_ptr) = URL::from_string(url_str.get()) else { + // Owns the C++ heap `WTF::URL`; `Drop` frees it on scope exit. + let Some(url) = URL::from_string(url_str.get()) else { return Ok(JSValue::UNDEFINED); }; - // SAFETY: URL::from_string returns an owned C++ heap pointer; freed exactly once via destroy(). - let _url_guard = scopeguard::guard(url_ptr, |p| unsafe { URL::destroy(p.as_ptr()) }); - // `_url_guard` keeps the C++ allocation live for this scope, so the - // `BackRef` liveness invariant holds; `Deref` encapsulates the single - // `NonNull::as_ref` site. - let url = bun_ptr::BackRef::from(url_ptr); let host: BunString = url.host(); let port_: u16 = { let port32 = url.port(); diff --git a/src/runtime/socket/UpgradedDuplex.rs b/src/runtime/socket/UpgradedDuplex.rs index 13e67a578edf..9435ef8c46a6 100644 --- a/src/runtime/socket/UpgradedDuplex.rs +++ b/src/runtime/socket/UpgradedDuplex.rs @@ -348,13 +348,12 @@ impl UpgradedDuplex { /// memoised `SecureContext` can be reused on the duplex/named-pipe path. pub(crate) fn start_tls_with_ctx( &mut self, - ctx: *mut bun_boringssl_sys::SSL_CTX, + ctx: *mut bun_boringssl_sys::sys::SSL_CTX, is_client: bool, ) -> Result<(), bun_core::Error> { // errdefer SSL_CTX_free(ctx) — free the adopted ref on the error path only. let ctx_guard = scopeguard::guard(ctx, |ctx| { - // SAFETY: ctx is a valid SSL_CTX* with one ref adopted by this fn. - unsafe { bun_boringssl_sys::SSL_CTX_free(ctx) }; + bun_boringssl_sys::SSL_CTX_free(bun_boringssl_sys::sys::SSL_CTX::opaque_ref(ctx)); }); let ctx_nn = NonNull::new(ctx).expect("caller passes a non-null SSL_CTX* with one adopted ref"); diff --git a/src/runtime/socket/WindowsNamedPipe.rs b/src/runtime/socket/WindowsNamedPipe.rs index 378d536f697b..f0a7d2995c0e 100644 --- a/src/runtime/socket/WindowsNamedPipe.rs +++ b/src/runtime/socket/WindowsNamedPipe.rs @@ -718,7 +718,7 @@ impl WindowsNamedPipe { pub fn get_accepted_by( &mut self, server: &mut uv::Pipe, - ssl_ctx: Option<*mut boringssl::SSL_CTX>, + ssl_ctx: Option<*mut boringssl::sys::SSL_CTX>, ) -> bun_sys::Result<()> { #[cfg(windows)] debug_assert!(self.pipe.is_some()); @@ -751,11 +751,8 @@ impl WindowsNamedPipe { }); } }; - // ref because we are accepting will unref when wrapper deinit. - // SAFETY: `tls_nn` proven non-null above - // (`NonNull::new(tls).expect(..)`); `SSL_CTX_up_ref` only bumps the - // atomic refcount on a live `SSL_CTX*`. - let _ = unsafe { boringssl::SSL_CTX_up_ref(tls_nn.as_ptr()) }; + // ref because we are accepting; unref'd when the wrapper deinits. + let _ = boringssl::SSL_CTX_up_ref(boringssl::sys::SSL_CTX::opaque_ref(tls_nn.as_ptr())); } #[cfg(windows)] { @@ -814,7 +811,7 @@ impl WindowsNamedPipe { &mut self, fd: Fd, ssl_options: Option, - owned_ctx: Option<*mut boringssl::SSL_CTX>, + owned_ctx: Option<*mut boringssl::sys::SSL_CTX>, ) -> bun_sys::Result<()> { debug_assert!(self.pipe.is_some()); self.flags.set_disconnected(true); @@ -856,7 +853,7 @@ impl WindowsNamedPipe { &mut self, path: &[u8], ssl_options: Option, - owned_ctx: Option<*mut boringssl::SSL_CTX>, + owned_ctx: Option<*mut boringssl::sys::SSL_CTX>, ) -> bun_sys::Result<()> { debug_assert!(self.pipe.is_some()); self.flags.set_disconnected(true); @@ -919,7 +916,7 @@ impl WindowsNamedPipe { &mut self, _fd: Fd, _ssl_options: Option, - _owned_ctx: Option<*mut boringssl::SSL_CTX>, + _owned_ctx: Option<*mut boringssl::sys::SSL_CTX>, ) -> bun_sys::Result<()> { // Unreachable on POSIX — `WindowsNamedPipeContext` is aliased to `()` there; // this stub exists only so the module type-checks across platforms. @@ -931,7 +928,7 @@ impl WindowsNamedPipe { &mut self, _path: &[u8], _ssl_options: Option, - _owned_ctx: Option<*mut boringssl::SSL_CTX>, + _owned_ctx: Option<*mut boringssl::sys::SSL_CTX>, ) -> bun_sys::Result<()> { // Unreachable on POSIX — see `open` above. unreachable!("WindowsNamedPipe::connect is windows-only") @@ -948,7 +945,7 @@ impl WindowsNamedPipe { fn init_tls_wrapper( &mut self, ssl_options: Option, - owned_ctx: Option<*mut boringssl::SSL_CTX>, + owned_ctx: Option<*mut boringssl::sys::SSL_CTX>, ) -> Option> { let handlers = ssl_wrapper::Handlers { ctx: std::ptr::from_mut(self), @@ -966,8 +963,8 @@ impl WindowsNamedPipe { self.wrapper = match WrapperType::init_with_ctx(ctx_nn, true, handlers) { Ok(w) => Some(w), Err(_) => { - // SAFETY: ctx is a valid SSL_CTX* with one adopted ref - unsafe { boringssl::SSL_CTX_free(ctx) }; + // One adopted ref, given back here. + boringssl::SSL_CTX_free(boringssl::sys::SSL_CTX::opaque_ref(ctx)); return Some(bun_sys::Result::Err(bun_sys::Error { errno: bun_sys::E::EPIPE as _, syscall: bun_sys::Tag::connect, diff --git a/src/runtime/socket/WindowsNamedPipeContext.rs b/src/runtime/socket/WindowsNamedPipeContext.rs index 8084ebbb88b2..4e3fd7b33447 100644 --- a/src/runtime/socket/WindowsNamedPipeContext.rs +++ b/src/runtime/socket/WindowsNamedPipeContext.rs @@ -406,7 +406,7 @@ impl WindowsNamedPipeContext { global_this: &JSGlobalObject, fd: Fd, ssl_config: Option, - owned_ctx: Option<*mut boringssl::SSL_CTX>, + owned_ctx: Option<*mut boringssl::sys::SSL_CTX>, socket: SocketType, ) -> Result<*mut WindowsNamedPipe, bun_core::Error> { // TODO: reuse the same context for multiple connections when possibles @@ -429,7 +429,7 @@ impl WindowsNamedPipeContext { global_this: &JSGlobalObject, path: &[u8], ssl_config: Option, - owned_ctx: Option<*mut boringssl::SSL_CTX>, + owned_ctx: Option<*mut boringssl::sys::SSL_CTX>, socket: SocketType, ) -> Result<*mut WindowsNamedPipe, bun_core::Error> { // TODO: reuse the same context for multiple connections when possibles diff --git a/src/runtime/socket/socket_body.rs b/src/runtime/socket/socket_body.rs index a2e71d26d3b5..66a19d8098fe 100644 --- a/src/runtime/socket/socket_body.rs +++ b/src/runtime/socket/socket_body.rs @@ -12,7 +12,7 @@ use bun_ptr::IntrusiveRc; // do NOT `use bun_boringssl_sys::SSL` here — it shadows the // `const SSL: bool` generic param in `NewSocket` below, making rustc // resolve `` as a type arg (E0747). Use the qualified path instead. -use bun_boringssl_sys::SSL_CTX; +use bun_boringssl_sys::sys::SSL_CTX; use bun_collections::VecExt; use bun_core::{self, fmt as bun_fmt}; use bun_jsc::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsRef, JsResult, SystemError}; @@ -1165,7 +1165,7 @@ impl NewSocket { } if let Some(promise) = handlers.take_promise() { // reject the promise on connect() error - let js_promise = jsc::JSPromise::opaque_mut(promise.as_promise().unwrap()); + let js_promise = jsc::JSPromise::opaque_ref(promise.as_promise().unwrap()); let err_value = err.to_error_instance_with_async_stack(&global, js_promise); js_promise.reject(&global, Ok(err_value))?; } else { @@ -1199,7 +1199,7 @@ impl NewSocket { } else if let Some(val) = handlers.take_promise() { // They've defined a `connectError` callback // The error is effectively handled, but we should still reject the promise. - let promise = jsc::JSPromise::opaque_mut(JSValue::as_promise(val).unwrap()); + let promise = jsc::JSPromise::opaque_ref(JSValue::as_promise(val).unwrap()); let err_ = err_for_promise .take() .to_error_instance_with_async_stack(&global, promise); @@ -3051,8 +3051,8 @@ impl NewSocket { drop(connection); } if let Some(ctx) = this_ref.owned_ssl_ctx.take() { - // SAFETY: BoringSSL FFI; we hold one owned ref. - unsafe { boringssl_sys::SSL_CTX_free(ctx) }; + // We hold one owned ref; give it back. + boringssl_sys::SSL_CTX_free(SSL_CTX::opaque_ref(ctx)); } // SAFETY: `this` was heap-allocated in `new()`. drop(unsafe { bun_core::heap::take(this) }); @@ -3211,7 +3211,7 @@ impl NewSocket { // `tls:` options. Either way `owned_ctx` holds one ref we drop in // deinit; SSL_new() takes its own. // - let owned_ctx: Option; + let owned_ctx: Option; let mut ssl_opts: Option = None; // Drop frees ssl_opts. @@ -3241,8 +3241,7 @@ impl NewSocket { }; // `borrow()` returns a +1 ref (it calls `SSL_CTX_up_ref`). // SAFETY: that ref is ours to release. - owned_ctx = - unsafe { boringssl_sys::OwnedSslCtx::from_raw(sc.borrow().cast::()) }; + owned_ctx = unsafe { boringssl_sys::SSL_CTX::adopt_ptr(sc.borrow()) }; // servername / ALPN still come from the surrounding tls config. if let Some(t) = opts.get_truthy(global, "tls")? { if !t.is_boolean() { @@ -3284,7 +3283,7 @@ impl NewSocket { }; owned_ctx = match cache.with_mut(|c| c.get_or_create(cfg, &mut create_err)) { // SAFETY: `get_or_create` hands back a +1 ref. - Some(c) => unsafe { boringssl_sys::OwnedSslCtx::from_raw(c.cast::()) }, + Some(c) => unsafe { boringssl_sys::SSL_CTX::adopt_ptr(c) }, None => { // us_ssl_ctx_from_options only sets *err for the CA/cipher // cases; bad cert/key/DH return NULL with err==.none and the @@ -3316,7 +3315,7 @@ impl NewSocket { let vm = handlers.vm; // The +1 `SSL_CTX` ref transfers into `tls.owned_ssl_ctx` below. - let owned_ctx_taken = owned_ctx.map(|c| c.into_raw()); + let owned_ctx_taken = owned_ctx.map(|c| c.leak().as_ptr()); let cfg = ssl_opts.as_ref(); let tls: bun_ptr::ThisPtr = TLSSocket::new(TLSSocket { @@ -3449,7 +3448,7 @@ impl NewSocket { tls.twin .set(Some(unsafe { IntrusiveRc::from_raw(raw.as_ptr()) })); // S008: `us_socket_t` is an `opaque_ffi!` ZST — safe deref. - bun_opaque::opaque_deref_mut(new_raw.as_ptr()).set_ssl_raw_tap(true); + bun_opaque::opaque_deref(new_raw.as_ptr()).set_ssl_raw_tap(true); let tls_js_value = tls.get_this_value(global); let raw_js_value = raw_ref.get_this_value(global); @@ -3471,17 +3470,17 @@ impl NewSocket { // it before ext was repointed would have ALPN/onOpen land in the // dead TCPSocket. TLSSocket::on_open(tls, tls.socket.get()); - bun_opaque::opaque_deref_mut(new_raw.as_ptr()).start_tls_handshake(); + bun_opaque::opaque_deref(new_raw.as_ptr()).start_tls_handshake(); // The socket being wrapped may have had its readable interest off (an // accepted socket nobody was reading yet — its ClientHello is still in // the kernel buffer); make sure the adopted TLS socket is reading so // the handshake can be driven. A no-op when it was already reading. - bun_opaque::opaque_deref_mut(new_raw.as_ptr()).resume(); + bun_opaque::opaque_deref(new_raw.as_ptr()).resume(); // Feed bytes that arrived before the upgrade (already pulled off the fd // by the plain-TCP layer) into the TLS engine exactly as if they had // just been received — for a server-side wrap this is the ClientHello. if !initial_data.is_empty() { - bun_opaque::opaque_deref_mut(new_raw.as_ptr()).tls_feed(initial_data.as_slice()); + bun_opaque::opaque_deref(new_raw.as_ptr()).tls_feed(initial_data.as_slice()); } let array = JSValue::create_empty_array(global, 2)?; @@ -4130,8 +4129,8 @@ impl DuplexUpgradeContext { // Close raced ahead of StartTLS — drop the unconsumed config. self.ssl_config = None; if let Some(ctx) = self.owned_ctx.take() { - // SAFETY: BoringSSL FFI; we hold one owned ref. - unsafe { boringssl_sys::SSL_CTX_free(ctx) }; + // We hold one owned ref; give it back. + boringssl_sys::SSL_CTX_free(SSL_CTX::opaque_ref(ctx)); } } } @@ -4191,7 +4190,7 @@ pub fn js_upgrade_duplex_to_tls( // duplex/named-pipe path shares one `SSL_CTX_new` with everyone else. // node:net wraps `[buntls]`'s return as `opts.tls.secureContext`; userland // may also pass it top-level. Same lookup as `upgradeTLS` above. - let mut owned_ctx: Option = None; + let mut owned_ctx: Option = None; let sc_js: JSValue = 'blk: { if let Some(v) = opts.get_truthy(global, "secureContext")? { break 'blk v; @@ -4215,7 +4214,7 @@ pub fn js_upgrade_duplex_to_tls( }; // `borrow()` returns a +1 ref (it calls `SSL_CTX_up_ref`). // SAFETY: that ref is ours to release. - owned_ctx = unsafe { boringssl_sys::OwnedSslCtx::from_raw(sc.borrow().cast::()) }; + owned_ctx = unsafe { boringssl_sys::SSL_CTX::adopt_ptr(sc.borrow()) }; } // Still parse SSLConfig for servername/ALPN (those live on the JS-side @@ -4267,7 +4266,7 @@ pub fn js_upgrade_duplex_to_tls( TLSSocket::data_set_cached(tls_js_value, global, default_data); // The +1 `SSL_CTX` ref transfers into `DuplexUpgradeContext.owned_ctx` below. - let owned_ctx_taken = owned_ctx.map(|c| c.into_raw()); + let owned_ctx_taken = owned_ctx.map(|c| c.leak().as_ptr()); // `DuplexUpgradeContext` is self-referential: `task.ctx` and // `upgrade.handlers.ctx` both point at the containing allocation, and diff --git a/src/runtime/socket/tls_socket_functions.rs b/src/runtime/socket/tls_socket_functions.rs index bad552304938..038d2b734759 100644 --- a/src/runtime/socket/tls_socket_functions.rs +++ b/src/runtime/socket/tls_socket_functions.rs @@ -17,16 +17,52 @@ use crate::api::bun_x509 as X509; // ────────────────────────────────────────────────────────────────────────── #[allow(non_camel_case_types, non_upper_case_globals)] pub(super) mod ffi { - use super::boringssl::{SSL, SSL_CTX, X509, X509_STORE, X509_STORE_CTX, struct_stack_st_X509}; + // The C objects, not the owning handles: the extern decls below traffic in + // `sys::X509` / `sys::X509_STORE` / `sys::X509_STORE_CTX`, and callers adopt + // the `+1`s into `boringssl::{X509, X509_STORE, X509_STORE_CTX}`. + use super::boringssl::sys::{SSL_CTX, X509, X509_STORE, X509_STORE_CTX}; + use super::boringssl::{SSL, struct_stack_st_X509}; use core::ffi::{c_char, c_int, c_long, c_uint, c_void}; // Re-export the one decl whose `*const c_char` NUL-terminated arg keeps a // genuine caller precondition; the rest are re-declared `safe fn` below. pub(crate) use super::boringssl::SSL_set_tlsext_host_name; + /// The C object itself. Only the extern declarations below name this type; + /// all Rust code uses the owning [`SSL_SESSION`] handle. + pub(crate) mod sys { + bun_opaque::opaque_ffi! { + /// `struct ssl_session_st` (`typedef ... SSL_SESSION`). `&Self` is + /// ABI-identical to a non-null `SSL_SESSION*` and carries no + /// `noalias`/`readonly` — BoringSSL mutates the session (refcount, + /// ticket) through it. + pub struct SSL_SESSION; + } + } + + /// `SSL_SESSION_free` takes `*mut SSL_SESSION`; `foreign_handle!` needs a + /// `fn(&sys::SSL_SESSION)`. + fn ssl_session_free_release(session: &sys::SSL_SESSION) { + // SAFETY: the handle owns one ref on the session's refcount; `as_mut_ptr` + // derives write provenance from the `UnsafeCell` body. + unsafe { SSL_SESSION_free(session.as_mut_ptr()) } + } + + // `d2i_SSL_SESSION` parses and hands back a `+1` on the session's refcount. + bun_opaque::foreign_handle! { + /// Owned handle to a BoringSSL `SSL_SESSION`. + /// + /// Holds one ref on the C refcount; `Drop` gives it back. Every method + /// takes `&self`: a refcount is shared by definition. + /// + /// A session *borrowed* from a live connection (`SSL_get_session`) took + /// no ref and stays a raw `*mut sys::SSL_SESSION`. `SSL_set_session` + /// takes its *own* reference, so it borrows too. + pub(crate) struct SSL_SESSION(sys::SSL_SESSION) via ssl_session_free_release; + } + // Opaque handles missing from boringssl_sys. bun_opaque::opaque_ffi! { - pub(crate) struct SSL_SESSION; pub(crate) struct SSL_CIPHER; pub(crate) struct EVP_PKEY; pub(crate) struct EC_KEY; @@ -103,27 +139,32 @@ pub(super) mod ffi { pub(crate) safe fn SSL_get_privatekey(ssl: &SSL) -> *mut EVP_PKEY; // ── SSL_SESSION ─────────────────────────────────────────────────── - pub(crate) safe fn SSL_get_session(ssl: &SSL) -> *mut SSL_SESSION; + // Borrowed from the live connection: takes no ref. + pub(crate) safe fn SSL_get_session(ssl: &SSL) -> *mut sys::SSL_SESSION; // Both handles are opaque-ZST refs (`UnsafeCell` body); BoringSSL bumps // `session`'s refcount internally — no caller-side precondition. - pub(crate) safe fn SSL_set_session(ssl: &SSL, session: &SSL_SESSION) -> c_int; + pub(crate) safe fn SSL_set_session(ssl: &SSL, session: &sys::SSL_SESSION) -> c_int; + // Releases one ref. Private: the only caller is `ssl_session_free_release`, + // which backs the `SSL_SESSION` handle's `Drop`. // SAFETY (unsafe fn): consumes a +1 reference; `session` must be uniquely owned or null. - pub(crate) fn SSL_SESSION_free(session: *mut SSL_SESSION); - // Opaque-ZST `&SSL_SESSION` + `&mut` out-params (FFI-nonnull) ⇒ no + fn SSL_SESSION_free(session: *mut sys::SSL_SESSION); + // Opaque-ZST `&sys::SSL_SESSION` + `&mut` out-params (FFI-nonnull) ⇒ no // caller-side precondition; BoringSSL writes a borrowed ptr/len pair. pub(crate) safe fn SSL_SESSION_get0_ticket( - session: &SSL_SESSION, + session: &sys::SSL_SESSION, out_ticket: &mut *const u8, out_len: &mut usize, ); // SAFETY (unsafe fn): `pp` (when non-null) must point to a buffer with capacity for the encoded session. - pub(crate) fn i2d_SSL_SESSION(session: *mut SSL_SESSION, pp: *mut *mut u8) -> c_int; + pub(crate) fn i2d_SSL_SESSION(session: *mut sys::SSL_SESSION, pp: *mut *mut u8) -> c_int; + // Allocates and returns a fresh +1 on success; null on a parse failure, + // having transferred nothing (`a` is always null at our call site). // SAFETY (unsafe fn): `*pp` must be readable for `length` bytes. pub(crate) fn d2i_SSL_SESSION( - a: *mut *mut SSL_SESSION, + a: *mut *mut sys::SSL_SESSION, pp: *mut *const u8, length: c_long, - ) -> *mut SSL_SESSION; + ) -> *mut sys::SSL_SESSION; // ── SSL_CIPHER ──────────────────────────────────────────────────── pub(crate) safe fn SSL_get_current_cipher(ssl: &SSL) -> *const SSL_CIPHER; @@ -243,13 +284,14 @@ pub(super) mod ffi { // object stack and `OPENSSL_sk_num(NULL)` returns 0. pub(crate) fn X509_STORE_get0_objects(store: *mut X509_STORE) -> *mut c_void; pub(crate) fn OPENSSL_sk_num(sk: *const c_void) -> usize; - // The process-wide default root store; up-refs before returning, so - // the caller owns a reference it must release with X509_STORE_free. + // The process-wide default root store; up-refs before returning, so the + // caller owns a reference — adopted into a `boringssl::X509_STORE`, whose + // `Drop` calls `X509_STORE_free`. pub(crate) fn us_get_shared_default_ca_store() -> *mut X509_STORE; - pub(crate) fn X509_STORE_free(store: *mut X509_STORE); - // X509_STORE_CTX lifecycle for issuer lookups; `new` allocates, - // `init` borrows the store, `free` releases. Used to extend the peer - // certificate chain through the local trust store. + // X509_STORE_CTX lifecycle for issuer lookups; `new` allocates and `init` + // borrows the store. The allocation is adopted into a + // `boringssl::X509_STORE_CTX`, whose `Drop` calls `X509_STORE_CTX_free`. + // Used to extend the peer certificate chain through the local trust store. pub(crate) fn X509_STORE_CTX_new() -> *mut X509_STORE_CTX; pub(crate) fn X509_STORE_CTX_init( ctx: *mut X509_STORE_CTX, @@ -257,7 +299,6 @@ pub(super) mod ffi { x509: *mut X509, chain: *mut struct_stack_st_X509, ) -> c_int; - pub(crate) fn X509_STORE_CTX_free(ctx: *mut X509_STORE_CTX); // Writes a +1 X509 reference to `*issuer` on success (> 0). pub(crate) fn X509_STORE_CTX_get1_issuer( issuer: *mut *mut X509, @@ -355,8 +396,11 @@ pub(super) fn get_peer_x509_certificate( return Ok(JSValue::UNDEFINED); }; let cert = ffi::SSL_get_peer_certificate(boringssl::SSL::opaque_ref(ssl_ptr)); - if !cert.is_null() { - return X509::to_js_object(boringssl::X509::opaque_mut(cert), global); + // SAFETY: `SSL_get_peer_certificate` up-refs the peer leaf and hands back a + // fresh `+1` (or null, having released nothing). This handle is the sole + // owner of that ref, and `to_js_object` gives it to C++'s `X509Pointer`. + if let Some(cert) = unsafe { boringssl::X509::adopt_ptr(cert) } { + return X509::to_js_object(cert, global); } Ok(JSValue::UNDEFINED) } @@ -370,10 +414,14 @@ pub(super) fn get_x509_certificate( return Ok(JSValue::UNDEFINED); }; let cert = ffi::SSL_get_certificate(boringssl::SSL::opaque_ref(ssl_ptr)); - if !cert.is_null() { - // X509_up_ref bumps the refcount before handing to JS. - ffi::X509_up_ref(boringssl::X509::opaque_ref(cert)); - return X509::to_js_object(boringssl::X509::opaque_mut(cert), global); + if let Some(cert) = core::ptr::NonNull::new(cert) { + // `SSL_get_certificate` only borrows; `X509_up_ref` mints the ref the + // handle owns and `to_js_object` hands to C++'s `X509Pointer`. + ffi::X509_up_ref(boringssl::sys::X509::opaque_ref(cert.as_ptr())); + // SAFETY: the `X509_up_ref` above added exactly one ref, and nothing + // else gives it back. + let cert = unsafe { boringssl::X509::adopt(cert) }; + return X509::to_js_object(cert, global); } Ok(JSValue::UNDEFINED) } @@ -459,13 +507,13 @@ pub(super) fn get_peer_certificate( if abbreviated { if this.is_server() { - // SSL_get_peer_certificate returns a +1 reference; we must free it. // X509::to_js only borrows the pointer (X509View is non-owning). let cert = ffi::SSL_get_peer_certificate(boringssl::SSL::opaque_ref(ssl_ptr)); - if !cert.is_null() { - // SAFETY: `c` is the +1 X509 reference returned by SSL_get_peer_certificate; we own it. - let _guard = scopeguard::guard(cert, |c| unsafe { boringssl::X509_free(c) }); - return X509::to_js(boringssl::X509::opaque_mut(cert), global); + // SAFETY: `SSL_get_peer_certificate` hands back a fresh `+1` (or + // null, having released nothing). This handle is its sole owner; its + // `Drop` gives the ref back once `to_js` has copied what it needs. + if let Some(cert) = unsafe { boringssl::X509::adopt_ptr(cert) } { + return X509::to_js(boringssl::sys::X509::opaque_mut(cert.as_ptr()), global); } } @@ -477,23 +525,29 @@ pub(super) fn get_peer_certificate( if cert.is_null() { return Ok(JSValue::UNDEFINED); } - return X509::to_js(boringssl::X509::opaque_mut(cert), global); + return X509::to_js(boringssl::sys::X509::opaque_mut(cert), global); } - let mut cert: *mut boringssl::X509 = core::ptr::null_mut(); - if this.is_server() { - // SSL_get_peer_certificate returns a +1 reference; we must free it. - cert = ffi::SSL_get_peer_certificate(boringssl::SSL::opaque_ref(ssl_ptr)); - } - let _guard = scopeguard::guard(cert, |c| { - if !c.is_null() { - // SAFETY: `c` is the +1 X509 reference returned by SSL_get_peer_certificate; we own it. - unsafe { boringssl::X509_free(c) }; + // On the client path the leaf is borrowed from the chain, so there is no ref + // to own and this stays `None`. + let owned_peer: Option = if this.is_server() { + // SAFETY: on the server path `SSL_get_peer_certificate` hands back a fresh + // `+1` (or null, having released nothing); this handle is its sole owner and + // its `Drop` at function exit replaces the old `scopeguard`. + unsafe { + boringssl::X509::adopt_ptr(ffi::SSL_get_peer_certificate(boringssl::SSL::opaque_ref( + ssl_ptr, + ))) } - }); + } else { + None + }; + let cert: *mut boringssl::sys::X509 = owned_peer + .as_ref() + .map_or(core::ptr::null_mut(), |c| c.as_ptr()); let cert_chain = ffi::SSL_get_peer_cert_chain(boringssl::SSL::opaque_ref(ssl_ptr)); - let first_cert: *mut boringssl::X509 = if !cert.is_null() { + let first_cert: *mut boringssl::sys::X509 = if !cert.is_null() { cert } else if !cert_chain.is_null() { ffi::sk_X509_value(boringssl::struct_stack_st_X509::opaque_ref(cert_chain), 0) @@ -510,13 +564,13 @@ pub(super) fn get_peer_certificate( // Node's getPeerCertificate(true) does. SSL_get_peer_cert_chain includes // the leaf on the client side but not on the server side, where the +1 // peer certificate above is the leaf instead. - let first_obj = X509::to_js(boringssl::X509::opaque_mut(first_cert), global)?; + let first_obj = X509::to_js(boringssl::sys::X509::opaque_mut(first_cert), global)?; // Link each certificate to its predecessor immediately so every object in // the chain is reachable from the stack-rooted `first_obj` before the next // `X509::to_js` allocation can trigger a GC - a heap-backed Vec // is not stack-scanned. let mut prev_obj: JSValue = first_obj; - let mut last_cert: *mut boringssl::X509 = first_cert; + let mut last_cert: *mut boringssl::sys::X509 = first_cert; if !cert_chain.is_null() { let mut i: usize = if cert.is_null() { 1 } else { 0 }; loop { @@ -525,7 +579,7 @@ pub(super) fn get_peer_certificate( if next.is_null() { break; } - let obj = X509::to_js(boringssl::X509::opaque_mut(next), global)?; + let obj = X509::to_js(boringssl::sys::X509::opaque_mut(next), global)?; prev_obj.put(global, b"issuerCertificate", obj); prev_obj = obj; last_cert = next; @@ -538,77 +592,81 @@ pub(super) fn get_peer_certificate( // X509_STORE_CTX_get1_issuer to surface the root that completed // verification even though the peer never sent it. let mut last_is_self_issued = false; - // SAFETY: the store ctx is created, initialized against the live SSL_CTX's - // store, used only within this scope and freed before returning; every - // issuer returned by get1_issuer is a +1 reference collected in `extras` - // and released after its fields have been copied into JS values and the - // terminal self-issued check has run. + // SAFETY: the store ctx is created and initialized against the live SSL_CTX's + // store, used only within this scope, and released by its handle's `Drop`; + // every issuer returned by get1_issuer is a +1 reference adopted into a + // handle collected in `extras` and released after its fields have been + // copied into JS values and the terminal self-issued check has run. unsafe { - let mut store = ffi::SSL_CTX_get_cert_store(boringssl::SSL_CTX::opaque_ref( + let mut store = ffi::SSL_CTX_get_cert_store(boringssl::sys::SSL_CTX::opaque_ref( ffi::SSL_get_SSL_CTX(boringssl::SSL::opaque_ref(ssl_ptr)), )); // A context built without an explicit `ca` (and without requestCert, // which installs the shared roots) carries an empty store and the // issuer walk would stop at whatever the peer sent. Fall back to the // process-wide default roots the way Node's per-context store always - // contains the bundled roots. The getter up-refs, so the temporary - // reference is released after the walk. - let mut shared_store: *mut boringssl::X509_STORE = core::ptr::null_mut(); - if store.is_null() || ffi::OPENSSL_sk_num(ffi::X509_STORE_get0_objects(store)) == 0 { - shared_store = ffi::us_get_shared_default_ca_store(); - if !shared_store.is_null() { - store = shared_store; - } + // contains the bundled roots. + // + // SAFETY: `us_get_shared_default_ca_store` up-refs the process-wide store + // before returning (or returns null, having released nothing), so this + // handle owns exactly one ref that nothing else gives back. It outlives + // the borrowed `store` pointer below and drops at the end of this block. + let shared_store: Option = + if store.is_null() || ffi::OPENSSL_sk_num(ffi::X509_STORE_get0_objects(store)) == 0 { + boringssl::X509_STORE::adopt_ptr(ffi::us_get_shared_default_ca_store()) + } else { + None + }; + if let Some(shared) = shared_store.as_ref() { + store = shared.as_ptr(); } - let store_ctx = ffi::X509_STORE_CTX_new(); - if !store_ctx.is_null() { + // SAFETY: `X509_STORE_CTX_new` allocates and hands back the sole owner, + // or null on OOM (having allocated nothing). + if let Some(store_ctx) = boringssl::X509_STORE_CTX::adopt_ptr(ffi::X509_STORE_CTX_new()) { if !store.is_null() && ffi::X509_STORE_CTX_init( - store_ctx, + store_ctx.as_ptr(), store, core::ptr::null_mut(), core::ptr::null_mut(), ) == 1 { - let mut extras: Vec<*mut boringssl::X509> = Vec::new(); + let mut extras: Vec = Vec::new(); // Cap the walk so a cyclic store cannot loop forever. while extras.len() < 16 && ffi::X509_check_issued(last_cert, last_cert) != 0 { - let mut issuer: *mut boringssl::X509 = core::ptr::null_mut(); - if ffi::X509_STORE_CTX_get1_issuer(&raw mut issuer, store_ctx, last_cert) <= 0 - || issuer.is_null() + let mut issuer: *mut boringssl::sys::X509 = core::ptr::null_mut(); + if ffi::X509_STORE_CTX_get1_issuer( + &raw mut issuer, + store_ctx.as_ptr(), + last_cert, + ) <= 0 { break; } - match X509::to_js(boringssl::X509::opaque_mut(issuer), global) { - Ok(obj) => { - prev_obj.put(global, b"issuerCertificate", obj); - prev_obj = obj; - } - Err(e) => { - boringssl::X509_free(issuer); - for extra in extras { - boringssl::X509_free(extra); - } - ffi::X509_STORE_CTX_free(store_ctx); - if !shared_store.is_null() { - ffi::X509_STORE_free(shared_store); - } - return Err(e); - } - } + // SAFETY: on success (> 0) `get1_issuer` wrote a fresh `+1` + // into `issuer`; this handle is its sole owner. A null slot + // carries no ref, so `None` just ends the walk (this is the + // old `|| issuer.is_null()` guard). + let Some(issuer) = boringssl::X509::adopt_ptr(issuer) else { + break; + }; + // On the `?` path `issuer`, then `extras`, then `store_ctx`, then + // `shared_store` all release on the way out — the same order the + // old hand-written error path used. + let obj = + X509::to_js(boringssl::sys::X509::opaque_mut(issuer.as_ptr()), global)?; + prev_obj.put(global, b"issuerCertificate", obj); + prev_obj = obj; + last_cert = issuer.as_ptr(); extras.push(issuer); - last_cert = issuer; } last_is_self_issued = ffi::X509_check_issued(last_cert, last_cert) == 0; - for extra in extras { - boringssl::X509_free(extra); - } + // `extras` drops here — after the terminal check has read + // `last_cert` — releasing every adopted issuer ref. } - ffi::X509_STORE_CTX_free(store_ctx); - } - if !shared_store.is_null() { - ffi::X509_STORE_free(shared_store); + // `store_ctx` drops here. } + // `shared_store` drops here. } // A self-issued terminal certificate references itself, like Node. @@ -626,10 +684,12 @@ pub(super) fn get_certificate( let Some(ssl_ptr) = this.socket.get().ssl() else { return Ok(JSValue::UNDEFINED); }; + // Borrowed from the connection: `SSL_get_certificate` takes no ref, and + // `X509::to_js` builds a non-owning `X509View`. let cert = ffi::SSL_get_certificate(boringssl::SSL::opaque_ref(ssl_ptr)); if !cert.is_null() { - return X509::to_js(boringssl::X509::opaque_mut(cert), global); + return X509::to_js(boringssl::sys::X509::opaque_mut(cert), global); } Ok(JSValue::UNDEFINED) } @@ -902,11 +962,11 @@ pub(crate) fn set_key_cert( ok_chain = ffi::SSL_set1_chain(ssl_ptr.cast(), chain); } if ok_cert != 1 || ok_key != 1 || ok_chain != 1 { - boringssl::SSL_CTX_free(ctx.cast()); + boringssl::SSL_CTX_free(boringssl::sys::SSL_CTX::opaque_ref(ctx)); return Err(global.throw(format_args!("setKeyCert failed to apply the context"))); } } - boringssl::SSL_CTX_free(ctx.cast()); + boringssl::SSL_CTX_free(boringssl::sys::SSL_CTX::opaque_ref(ctx)); } Ok(JSValue::UNDEFINED) } @@ -1171,16 +1231,18 @@ pub(super) fn set_session( c_long::try_from(session_slice.len()).expect("int cast"), ) }; - if session.is_null() { - return Ok(JSValue::UNDEFINED); - } // SSL_set_session takes its own reference ("the caller retains ownership of |session|"), - // so we must release the one returned by d2i_SSL_SESSION on every path. - // SAFETY: `s` is the +1 SSL_SESSION reference returned by d2i_SSL_SESSION; we own it. - let _guard = scopeguard::guard(session, |s| unsafe { ffi::SSL_SESSION_free(s) }); + // so the one d2i_SSL_SESSION handed us is released on every path by the + // handle's `Drop`. + // SAFETY: `d2i_SSL_SESSION` (with a null `a`) allocates and returns a + // fresh `+1` on success, or null on a parse failure, having transferred + // nothing. This handle is the sole owner of that ref. + let Some(session) = (unsafe { ffi::SSL_SESSION::adopt_ptr(session) }) else { + return Ok(JSValue::UNDEFINED); + }; if ffi::SSL_set_session( boringssl::SSL::opaque_ref(ssl_ptr), - ffi::SSL_SESSION::opaque_ref(session), + ffi::sys::SSL_SESSION::opaque_ref(session.as_ptr()), ) != 1 { return Err(global.throw_value(get_ssl_exception(global, b"SSL_set_session error"))); @@ -1209,7 +1271,7 @@ pub(super) fn get_tls_ticket( let mut length: usize = 0; // The pointer is only valid while the connection is in use so we need to copy it ffi::SSL_SESSION_get0_ticket( - ffi::SSL_SESSION::opaque_ref(session), + ffi::sys::SSL_SESSION::opaque_ref(session), &mut ticket, &mut length, ); @@ -1315,7 +1377,9 @@ pub(super) fn set_verify_mode( extern "C" fn always_allow_ssl_verify_callback( _preverify_ok: c_int, - _ctx: *mut boringssl::X509_STORE_CTX, + // BoringSSL owns the context it passes in; this is the C object, not the + // owning `boringssl::X509_STORE_CTX` handle. + _ctx: *mut boringssl::sys::X509_STORE_CTX, ) -> c_int { 1 } diff --git a/src/runtime/socket/udp_socket.rs b/src/runtime/socket/udp_socket.rs index 72c606d90ad1..bde270e4b55a 100644 --- a/src/runtime/socket/udp_socket.rs +++ b/src/runtime/socket/udp_socket.rs @@ -495,8 +495,8 @@ impl UDPSocket { /// fields are `Cell`/`JsCell`, so a shared borrow is sufficient (R-2). #[inline] fn from_uws<'a>(socket: *mut uws::udp::Socket) -> &'a UDPSocket { - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - let user = uws::udp::Socket::opaque_mut(socket).user(); + // `Socket` is an `opaque_ffi!` ZST — `opaque_ref` is the safe deref. + let user = uws::udp::Socket::opaque_ref(socket).user(); // SAFETY: `user` was set to `*mut UDPSocket` at creation; non-null and // live for the callback's duration (back-ref invariant). unsafe { &*user.cast::() } @@ -544,8 +544,8 @@ impl UDPSocket { // repeats it), so ordering is unobservable. this.this_value.with_mut(|r| r.downgrade()); if let Some(socket) = this.socket.take() { - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - uws::udp::Socket::opaque_mut(socket).close(); + // `Socket` is an `opaque_ffi!` ZST — `opaque_ref` is the safe deref. + uws::udp::Socket::opaque_ref(socket).close(); } }); @@ -622,8 +622,8 @@ impl UDPSocket { if let Some(connect) = &this.config.get().connect { let address_z = connect.address.to_owned_slice_z(); - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - let ret = uws::udp::Socket::opaque_mut(this.socket.get().unwrap()) + // `Socket` is an `opaque_ffi!` ZST — `opaque_ref` is the safe deref. + let ret = uws::udp::Socket::opaque_ref(this.socket.get().unwrap()) .connect(address_z.as_ptr(), connect.port as u32); if ret != 0 { if let Some(sys_err) = errno_sys(ret, bun_sys::Tag::connect) { @@ -719,8 +719,8 @@ impl UDPSocket { .to_js(global_this), )); }; - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - let res = uws::udp::Socket::opaque_mut(socket).set_broadcast(enabled); + // `Socket` is an `opaque_ffi!` ZST — `opaque_ref` is the safe deref. + let res = uws::udp::Socket::opaque_ref(socket).set_broadcast(enabled); if let Some(err) = get_us_error::(res, bun_sys::Tag::setsockopt) { return Err(global_this.throw_value(err.to_js(global_this))); @@ -767,8 +767,8 @@ impl UDPSocket { .to_js(global_this), )); }; - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - let res = uws::udp::Socket::opaque_mut(socket).set_multicast_loopback(enabled); + // `Socket` is an `opaque_ffi!` ZST — `opaque_ref` is the safe deref. + let res = uws::udp::Socket::opaque_ref(socket).set_multicast_loopback(enabled); if let Some(err) = get_us_error::(res, bun_sys::Tag::setsockopt) { return Err(global_this.throw_value(err.to_js(global_this))); @@ -835,10 +835,10 @@ impl UDPSocket { "Family mismatch between address and interface" ))); } - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - uws::udp::Socket::opaque_mut(socket).set_membership(&addr, Some(&interface), drop) + // `Socket` is an `opaque_ffi!` ZST — `opaque_ref` is the safe deref. + uws::udp::Socket::opaque_ref(socket).set_membership(&addr, Some(&interface), drop) } else { - uws::udp::Socket::opaque_mut(socket).set_membership(&addr, None, drop) + uws::udp::Socket::opaque_ref(socket).set_membership(&addr, None, drop) }; if let Some(err) = get_us_error::(res, bun_sys::Tag::setsockopt) { @@ -949,15 +949,15 @@ impl UDPSocket { "Family mismatch among source, group and interface addresses" ))); } - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - uws::udp::Socket::opaque_mut(socket).set_source_specific_membership( + // `Socket` is an `opaque_ffi!` ZST — `opaque_ref` is the safe deref. + uws::udp::Socket::opaque_ref(socket).set_source_specific_membership( &source_addr, &group_addr, Some(&interface), drop, ) } else { - uws::udp::Socket::opaque_mut(socket).set_source_specific_membership( + uws::udp::Socket::opaque_ref(socket).set_source_specific_membership( &source_addr, &group_addr, None, @@ -1036,8 +1036,8 @@ impl UDPSocket { return Err(global_this.throw(format_args!("Socket is closed"))); }; - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - let res = uws::udp::Socket::opaque_mut(socket).set_multicast_interface(&addr); + // `Socket` is an `opaque_ffi!` ZST — `opaque_ref` is the safe deref. + let res = uws::udp::Socket::opaque_ref(socket).set_multicast_interface(&addr); if let Some(err) = get_us_error::(res, bun_sys::Tag::setsockopt) { return Err(global_this.throw_value(err.to_js(global_this))); @@ -1078,7 +1078,7 @@ impl UDPSocket { this: &Self, global_this: &JSGlobalObject, callframe: &CallFrame, - function: fn(&mut uws::udp::Socket, i32) -> c_int, + function: fn(&uws::udp::Socket, i32) -> c_int, ) -> JsResult { if this.closed.get() { return Err(global_this.throw_value( @@ -1102,8 +1102,8 @@ impl UDPSocket { let Some(socket) = this.socket.get() else { return Err(global_this.throw(format_args!("Socket is closed"))); }; - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - let res = function(uws::udp::Socket::opaque_mut(socket), ttl); + // `Socket` is an `opaque_ffi!` ZST — `opaque_ref` is the safe deref. + let res = function(uws::udp::Socket::opaque_ref(socket), ttl); if let Some(err) = get_us_error::(res, bun_sys::Tag::setsockopt) { return Err(global_this.throw_value(err.to_js(global_this))); @@ -1314,8 +1314,8 @@ impl UDPSocket { let Some(socket) = this.socket.get() else { return Err(global_this.throw(format_args!("Socket is closed"))); }; - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - let res = uws::udp::Socket::opaque_mut(socket).send(&payloads, &lens, &addr_ptrs); + // `Socket` is an `opaque_ffi!` ZST — `opaque_ref` is the safe deref. + let res = uws::udp::Socket::opaque_ref(socket).send(&payloads, &lens, &addr_ptrs); if let Some(err) = get_us_error::(res, bun_sys::Tag::send) { return Err(global_this.throw_value(err.to_js(global_this))); } @@ -1411,8 +1411,8 @@ impl UDPSocket { let Some(socket) = this.socket.get() else { return Err(global_this.throw(format_args!("Socket is closed"))); }; - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - let res = uws::udp::Socket::opaque_mut(socket).send( + // `Socket` is an `opaque_ffi!` ZST — `opaque_ref` is the safe deref. + let res = uws::udp::Socket::opaque_ref(socket).send( &[payload.as_ptr()], &[payload.len()], &[addr_ptr], @@ -1582,8 +1582,8 @@ impl UDPSocket { // shared borrow is sound; the (idempotent) downgrade is hoisted // because `on_close` repeats it. this.this_value.with_mut(|r| r.downgrade()); - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - uws::udp::Socket::opaque_mut(socket).close(); + // `Socket` is an `opaque_ffi!` ZST — `opaque_ref` is the safe deref. + uws::udp::Socket::opaque_ref(socket).close(); } Ok(JSValue::UNDEFINED) @@ -1630,8 +1630,8 @@ impl UDPSocket { let Some(socket) = this.socket.get() else { return JSValue::UNDEFINED; }; - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - JSValue::js_number(uws::udp::Socket::opaque_mut(socket).bound_port() as f64) + // `Socket` is an `opaque_ffi!` ZST — `opaque_ref` is the safe deref. + JSValue::js_number(uws::udp::Socket::opaque_ref(socket).bound_port() as f64) } fn create_sock_addr(global_this: &JSGlobalObject, address_bytes: &[u8], port: u16) -> JSValue { @@ -1652,8 +1652,8 @@ impl UDPSocket { }; let mut buf = [0u8; 64]; let mut length: i32 = 64; - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - let socket = uws::udp::Socket::opaque_mut(socket); + // `Socket` is an `opaque_ffi!` ZST — `opaque_ref` is the safe deref. + let socket = uws::udp::Socket::opaque_ref(socket); socket.bound_ip(buf.as_mut_ptr(), &mut length); let address_bytes = &buf[..usize::try_from(length).expect("int cast")]; @@ -1678,8 +1678,8 @@ impl UDPSocket { }; let mut buf = [0u8; 64]; let mut length: i32 = 64; - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - uws::udp::Socket::opaque_mut(socket).remote_ip(buf.as_mut_ptr(), &mut length); + // `Socket` is an `opaque_ffi!` ZST — `opaque_ref` is the safe deref. + uws::udp::Socket::opaque_ref(socket).remote_ip(buf.as_mut_ptr(), &mut length); let address_bytes = &buf[..usize::try_from(length).expect("int cast")]; Self::create_sock_addr(global_this, address_bytes, connect_info.port) @@ -1805,8 +1805,8 @@ impl UDPSocket { return Err(global_object.throw(format_args!("Socket is closed"))); } - // `Socket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe deref. - if uws::udp::Socket::opaque_mut(this.socket.get().unwrap()).disconnect() == -1 { + // `Socket` is an `opaque_ffi!` ZST — `opaque_ref` is the safe deref. + if uws::udp::Socket::opaque_ref(this.socket.get().unwrap()).disconnect() == -1 { return Err(global_object.throw(format_args!("Failed to disconnect socket"))); } this.connect_info.set(None); diff --git a/src/runtime/socket/uws_dispatch.rs b/src/runtime/socket/uws_dispatch.rs index 508b43ab029c..ca6bdd225894 100644 --- a/src/runtime/socket/uws_dispatch.rs +++ b/src/runtime/socket/uws_dispatch.rs @@ -99,9 +99,9 @@ fn vt(s: *mut us_socket_t) -> &'static VTable { #[inline] fn vtc(c: *mut ConnectingSocket) -> &'static VTable { - // `ConnectingSocket` is an `opaque_ffi!` ZST — `opaque_mut` is the safe + // `ConnectingSocket` is an `opaque_ffi!` ZST — `opaque_ref` is the safe // deref (loop.c only dispatches live, non-null connecting sockets). - let c = ConnectingSocket::opaque_mut(c); + let c = ConnectingSocket::opaque_ref(c); let kind = c.kind(); match kind { SocketKind::Invalid => { diff --git a/src/runtime/test_runner/ScopeFunctions.rs b/src/runtime/test_runner/ScopeFunctions.rs index c7aba07555d6..f3c0ac610d67 100644 --- a/src/runtime/test_runner/ScopeFunctions.rs +++ b/src/runtime/test_runner/ScopeFunctions.rs @@ -423,9 +423,10 @@ impl ScopeFunctions { "matches_filter \"{}\"", bstr::BStr::new(bun_test.collection.filter_buffer.as_slice()) )); - // `RegularExpression` is an `opaque_ffi!` ZST handle; `opaque_mut` is - // the centralised non-null deref proof. - matches_filter = RegularExpression::opaque_mut(filter_regex.as_ptr()).matches(str); + // SAFETY: `filter_regex` was leaked into `TestOptions` by + // `cli::Arguments` and lives for the process; the ManuallyDrop + // borrow frees nothing. + matches_filter = unsafe { RegularExpression::borrow_leaked(filter_regex) }.matches(str); bun_test.collection.filter_buffer.clear(); } diff --git a/src/runtime/test_runner/jest.rs b/src/runtime/test_runner/jest.rs index 7e20e6fe6125..335ea40f94f3 100644 --- a/src/runtime/test_runner/jest.rs +++ b/src/runtime/test_runner/jest.rs @@ -6,9 +6,7 @@ use crate::cli::test_command::CommandLineReporter; use bun_collections::{ArrayHashMap, MultiArrayList}; use bun_core::Output; use bun_jsc::virtual_machine::VirtualMachine; -use bun_jsc::{ - self as jsc, CallFrame, JSGlobalObject, JSValue, JsResult, RegularExpression, -}; +use bun_jsc::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsResult}; use bun_jsc::StringJsc as _; use crate::timer::ElTimespec; @@ -143,10 +141,10 @@ pub struct TestRunner<'a> { pub test_options: &'a TestOptions, /// Used for --test-name-pattern to reduce allocations. - /// Raw `*mut` because `RegularExpression::matches` mutates its internal - /// cursor through C++ — storing `&'a RegularExpression` and casting back to - /// `*mut` at the use site would launder shared provenance into a write (UB). - pub filter_regex: Option>, + /// Points at the C++ regex (`sys`), not the owning `RegularExpression` handle: + /// `cli::Arguments` leaked the allocation into `TestOptions`, so this is a + /// non-owning borrow, taken with `RegularExpression::borrow_leaked`. + pub filter_regex: Option>, pub unhandled_errors_between_tests: u32, pub summary: Summary, diff --git a/src/runtime/valkey_jsc/js_valkey.rs b/src/runtime/valkey_jsc/js_valkey.rs index 770fb8157e37..71d83ee0be82 100644 --- a/src/runtime/valkey_jsc/js_valkey.rs +++ b/src/runtime/valkey_jsc/js_valkey.rs @@ -1,6 +1,5 @@ use core::cell::Cell; use core::ffi::c_void; -use core::ptr::NonNull; use crate::socket::{SSLConfig, SSLConfigFromJs}; use bun_boringssl as boringssl; @@ -502,7 +501,8 @@ impl JSValkeyClient { // the case right now and I do not understand why. It will take some work in JSC to // understand why this is happening, but since I need to uncork valkey, I'm adding this as // a stop-gap. - let parsed_url: NonNull = 'get_url: { + // Owns the C++ heap `WTF::URL`; `Drop` frees it on scope exit. + let parsed_url: URL = 'get_url: { let url_slice = url_str.to_utf8(); let url_byte_slice = url_slice.slice(); @@ -547,13 +547,6 @@ impl JSValkeyClient { } } }; - // SAFETY: `from_utf8` heap-allocates; release on scope exit. - let _parsed_url_drop = - scopeguard::guard(parsed_url, |p| unsafe { URL::destroy(p.as_ptr()) }); - // `_parsed_url_drop` keeps the heap `URL` live for this scope, so the - // `BackRef` liveness invariant holds; `Deref` encapsulates the single - // `NonNull::as_ref` site. - let parsed_url = bun_ptr::BackRef::from(parsed_url); // Extract protocol string let protocol_str = parsed_url.protocol(); @@ -1288,9 +1281,9 @@ impl JSValkeyClient { if let Some(promise) = Js::connection_promise_get_cached(this_value) { Js::connection_promise_set_cached(this_value, &global_object, JSValue::ZERO); - // `JSPromise` is an `opaque_ffi!` ZST — `opaque_mut` is the + // `JSPromise` is an `opaque_ffi!` ZST — `opaque_ref` is the // safe deref. Cached slot held a valid JSPromise. - let js_promise = JSPromise::opaque_mut(promise.as_promise().unwrap()); + let js_promise = JSPromise::opaque_ref(promise.as_promise().unwrap()); if self.client.get().flags.connection_promise_returns_client { debug!("Resolving connection promise with client instance"); js_promise.resolve(&global_object, this_value)?; @@ -1437,9 +1430,9 @@ impl JSValkeyClient { if !this_jsvalue.is_undefined() { if let Some(promise) = Js::connection_promise_get_cached(this_jsvalue) { Js::connection_promise_set_cached(this_jsvalue, &global_object, JSValue::ZERO); - // `JSPromise` is an `opaque_ffi!` ZST — `opaque_mut` is the + // `JSPromise` is an `opaque_ffi!` ZST — `opaque_ref` is the // safe deref. Cached slot held a valid JSPromise. - JSPromise::opaque_mut(promise.as_promise().unwrap()) + JSPromise::opaque_ref(promise.as_promise().unwrap()) .reject(&global_object, Ok(error_value))?; } } @@ -1736,8 +1729,8 @@ impl JSValkeyClient { let this_ref = unsafe { &*this }; debug_assert!(this_ref.client.get().socket.is_closed()); if let Some(s) = this_ref._secure.get() { - // SAFETY: SSL_CTX is C-refcounted; this releases our ref. - unsafe { boringssl::c::SSL_CTX_free(s) }; + // SSL_CTX is C-refcounted; this releases our ref. + boringssl::c::SSL_CTX_free(uws::SslCtx::opaque_ref(s)); } this_ref.client_mut().shutdown(None); this_ref.poll_ref.with_mut(|r| r.disable()); diff --git a/src/runtime/webcore/Blob.rs b/src/runtime/webcore/Blob.rs index d6422a692257..09ac36004e2b 100644 --- a/src/runtime/webcore/Blob.rs +++ b/src/runtime/webcore/Blob.rs @@ -187,11 +187,11 @@ pub trait BlobExt { Self: Sized; fn from_url_search_params( global_this: &JSGlobalObject, - search_params: &mut jsc::URLSearchParams, + search_params: &jsc::URLSearchParams, ) -> Blob where Self: Sized; - fn from_dom_form_data(global_this: &JSGlobalObject, form_data: &mut jsc::DOMFormData) -> Blob + fn from_dom_form_data(global_this: &JSGlobalObject, form_data: &jsc::DOMFormData) -> Blob where Self: Sized; fn content_type(&self) -> &[u8]; @@ -859,7 +859,7 @@ impl BlobExt for Blob { } fn from_url_search_params( global_this: &JSGlobalObject, - search_params: &mut jsc::URLSearchParams, + search_params: &jsc::URLSearchParams, ) -> Blob { let mut converter = URLSearchParamsConverter { buf: Vec::new() }; search_params.to_string(&mut converter, URLSearchParamsConverter::convert); @@ -882,7 +882,7 @@ impl BlobExt for Blob { blob } - fn from_dom_form_data(global_this: &JSGlobalObject, form_data: &mut jsc::DOMFormData) -> Blob { + fn from_dom_form_data(global_this: &JSGlobalObject, form_data: &jsc::DOMFormData) -> Blob { // "----WebKitFormBoundary" (22 bytes) + 32 lowercase-hex chars of a fresh UUID. const BOUNDARY_PREFIX: &[u8; 22] = b"----WebKitFormBoundary"; let mut boundary_buf = [0u8; BOUNDARY_PREFIX.len() + 32]; @@ -942,13 +942,11 @@ impl BlobExt for Blob { ctx.on_entry(unsafe { *name_ }, entry); } unsafe extern "C" { - // `this` is the `&mut DOMFormData` param (coerced); `ctx`/`cb` are - // stored opaquely and only used synchronously. Module-private with - // one call site below — no caller-side precondition remains. Kept - // `*mut` (not `&mut`) to match the `bun_jsc` decl and avoid - // `clashing_extern_declarations`. + // `this` is the opaque `&DOMFormData` handle; `ctx`/`cb` are stored + // opaquely and only used synchronously. Module-private with one call + // site below — no caller-side precondition remains. safe fn DOMFormData__forEach( - this: *mut jsc::DOMFormData, + this: &jsc::DOMFormData, ctx: *mut c_void, // Safe fn-ptr: `for_each_thunk` is a safe `extern "C" fn` (its // body localises every raw deref individually), so the callback diff --git a/src/runtime/webcore/Body.rs b/src/runtime/webcore/Body.rs index 03ea5dadd526..97dcc09b0a62 100644 --- a/src/runtime/webcore/Body.rs +++ b/src/runtime/webcore/Body.rs @@ -56,17 +56,17 @@ fn set_blob_content_type(blob: &Blob, mime_type: MimeType) { // ──────────────────────────────────────────────────────────────────────────── #[inline] -fn as_dom_form_data<'a>(value: JSValue) -> Option<&'a mut DOMFormData> { +fn as_dom_form_data<'a>(value: JSValue) -> Option<&'a DOMFormData> { // `DOMFormData` is an opaque C++ type without a `#[bun_jsc::JsClass]` derive; // route through the hand-written `from_js` (`DOMFormData.rs`) instead of // `value.as_::()`. DOMFormData::from_js(value) } #[inline] -fn as_url_search_params<'a>(value: JSValue) -> Option<&'a mut URLSearchParams> { +fn as_url_search_params<'a>(value: JSValue) -> Option<&'a URLSearchParams> { // See `as_dom_form_data` — opaque C++ type, hand-written `from_js`. // `URLSearchParams` is an opaque ZST FFI handle (S008) — safe deref. - URLSearchParams::from_js(value).map(|p| bun_opaque::opaque_deref_mut(p.as_ptr())) + URLSearchParams::from_js(value).map(|p| bun_opaque::opaque_deref(p.as_ptr())) } bun_core::declare_scope!(BodyValue, visible); diff --git a/src/runtime/webcore/CookieMap.rs b/src/runtime/webcore/CookieMap.rs index 0a109ab74104..86e9796dd581 100644 --- a/src/runtime/webcore/CookieMap.rs +++ b/src/runtime/webcore/CookieMap.rs @@ -14,7 +14,7 @@ unsafe extern "C" { // wraps `UnsafeCell` so `&JSGlobalObject` permits C++ interior mutation. // `uws_http_response` is an opaque pass-through validated by the caller. safe fn CookieMap__write( - cookie_map: &mut CookieMap, + cookie_map: &CookieMap, global_this: &JSGlobalObject, kind: ResponseKind, uws_http_response: *mut c_void, @@ -26,8 +26,12 @@ unsafe extern "C" { } impl CookieMap { + /// `&self`, not `&mut self`: `CookieMap` is an `opaque_ffi!` ZST, so `&Self` + /// carries no `noalias`/`readonly` and C++ mutates the cookie store through + /// it. A `&mut` would assert an exclusivity that never holds — the C++ side + /// owns the object and other refs exist by definition. pub fn write( - &mut self, + &self, global_this: &JSGlobalObject, kind: ResponseKind, uws_http_response: *mut c_void, @@ -40,23 +44,27 @@ impl CookieMap { // NOTE: no inherent `ref`/`deref` on `CookieMap` — `CookieMapRef` is the // sanctioned owner of the intrusive C++ refcount. Exposing bare refcount - // mutators on the pointee would let `&mut *cookie_map_ref` corrupt the + // mutators on the pointee would let a borrow of the pointee corrupt the // count relative to the ref's owned `+1` (double-unref / UAF on Drop), // mirroring how `RefPtr` discourages bare `ref`/`deref` on its pointee. } -/// Intrusive smart pointer over a C++-refcounted `CookieMap`. -/// -/// Owns exactly one strong ref: `new_ref` bumps it (for a borrowed handle) -/// and `Drop` releases it. Mirrors `AbortSignalRef` — a raw FFI handle (opaque -/// C++ object) cannot live inside `Box`/`Arc`, so this newtype is the -/// sanctioned owning representation. -/// -/// (A `+1`-transfer constructor — adopting an already-bumped raw pointer -/// without a fresh `ref()` — is deliberately omitted until a caller needs it; -/// every construction site in the tree goes through `new_ref`.) -#[repr(transparent)] -pub struct CookieMapRef(NonNull); +// The ownership unit is one tick of the C++ intrusive refcount: `CookieMap__ref` +// adds one, `CookieMap__deref` gives one back. One `CookieMapRef` owns exactly +// one, taken in `new_ref` and released by `ForeignRef`'s `Drop`. +bun_opaque::foreign_handle! { + /// Intrusive smart pointer over a C++-refcounted `CookieMap`. + /// + /// Owns exactly one strong ref: [`CookieMapRef::new_ref`] bumps it (for a + /// borrowed handle) and `Drop` releases it. Mirrors `AbortSignalRef` — a raw + /// FFI handle (opaque C++ object) cannot live inside `Box`/`Arc`, so this + /// newtype is the sanctioned owning representation. + /// + /// The macro also emits `adopt`/`adopt_ptr` (`+1`-transfer constructors that + /// take an already-bumped pointer). No caller needs them yet: every + /// construction site in the tree goes through `new_ref`. + pub struct CookieMapRef(CookieMap) via CookieMap__deref; +} impl CookieMapRef { /// Bump the refcount of a borrowed `CookieMap` and wrap it (the caller @@ -64,12 +72,13 @@ impl CookieMapRef { #[inline] pub fn new_ref(cookie_map: &CookieMap) -> Self { CookieMap__ref(cookie_map); - Self(NonNull::from(cookie_map)) - } - - #[inline] - pub fn as_ptr(&self) -> *mut CookieMap { - self.0.as_ptr() + // SAFETY: the `CookieMap__ref` on the line above is the producer — it + // just added one strong ref to the C++ intrusive count, over and above + // the one `cookie_map` is borrowed from. Nothing else will give that + // unit back, so this handle adopts it and releases it exactly once in + // `Drop` (via `CookieMap__deref`). `as_mut_ptr()` returns the address of + // a live `&CookieMap`, hence non-null. + unsafe { Self::adopt(NonNull::new_unchecked(cookie_map.as_mut_ptr())) } } } @@ -77,16 +86,12 @@ impl core::ops::Deref for CookieMapRef { type Target = CookieMap; #[inline] fn deref(&self) -> &CookieMap { - CookieMap::opaque_ref(self.0.as_ptr()) + self.raw() } } -impl core::ops::DerefMut for CookieMapRef { - #[inline] - fn deref_mut(&mut self) -> &mut CookieMap { - CookieMap::opaque_mut(self.0.as_ptr()) - } -} +// No `DerefMut`: see `CookieMap::write` — `&mut CookieMap` would assert an +// exclusivity over a C++-owned object that is never true, and nothing needs it. impl Clone for CookieMapRef { #[inline] @@ -94,12 +99,3 @@ impl Clone for CookieMapRef { Self::new_ref(self) } } - -impl Drop for CookieMapRef { - #[inline] - fn drop(&mut self) { - // Held +1 ref keeps the C++ object alive until this deref; `Deref` - // (above) encapsulates the NonNull access. - CookieMap__deref(self) - } -} diff --git a/src/runtime/webcore/Crypto.rs b/src/runtime/webcore/Crypto.rs index 558419b5e3f4..b62b6602a939 100644 --- a/src/runtime/webcore/Crypto.rs +++ b/src/runtime/webcore/Crypto.rs @@ -32,7 +32,6 @@ impl Crypto { array_a: &JSUint8Array, array_b: &JSUint8Array, ) -> JSValue { - // `JSUint8Array::slice()` takes `&mut self`; use ptr/len (`&self`) instead. let a_ptr = array_a.ptr(); let b_ptr = array_b.ptr(); let len = array_a.len(); @@ -49,8 +48,8 @@ impl Crypto { return JSValue::ZERO; } - // SAFETY: a_ptr/b_ptr are valid for `len` bytes (just obtained from JSUint8Array; - // `JSUint8Array::slice()` needs `&mut self`, so reconstruct the slices here). + // SAFETY: a_ptr/b_ptr are valid for `len` bytes. Both arrays may be the same cell, + // so take shared slices rather than two `slice()` results. // `ffi::slice` tolerates `(null, 0)` for detached/empty arrays. let (a, b) = unsafe { ( @@ -95,8 +94,8 @@ impl Crypto { global: &JSGlobalObject, array: &JSUint8Array, ) -> JSValue { - // `JSUint8Array::slice()` takes - // `&mut self`; use ptr()/len() (which take `&self`) to avoid the &mut requirement. + // `JSUint8Array::slice()` takes `&mut self`; use ptr()/len() (which take + // `&self`) to avoid the &mut requirement. // SAFETY: JSC guarantees `ptr()` is valid for `len()` writable bytes while the // typed-array cell is alive; `ffi::slice_mut` tolerates `(null, 0)` for detached. random_data(global, unsafe { diff --git a/src/runtime/webcore/FormData.rs b/src/runtime/webcore/FormData.rs index adc22cfe638c..121181e5ad1a 100644 --- a/src/runtime/webcore/FormData.rs +++ b/src/runtime/webcore/FormData.rs @@ -212,7 +212,7 @@ pub fn to_js_from_multipart_data( struct Wrapper<'a> { global: &'a JSGlobalObject, - form: &'a mut DOMFormData, + form: &'a DOMFormData, } impl<'a> Wrapper<'a> { diff --git a/src/runtime/webcore/TextDecoder.rs b/src/runtime/webcore/TextDecoder.rs index ce2388805bd8..9f5fa33125ea 100644 --- a/src/runtime/webcore/TextDecoder.rs +++ b/src/runtime/webcore/TextDecoder.rs @@ -5,7 +5,6 @@ use crate::webcore::jsc::{ use bun_core::AllocError; use bun_core::{OwnedString, strings}; use core::cell::Cell; -use core::ptr::NonNull; use jsc::StringJsc as _; use jsc::ZigStringJsc as _; @@ -51,7 +50,7 @@ pub struct TextDecoder { // streaming state (lead byte, ISO-2022-JP mode, GB18030 first/second/third), // so it must live across `{stream: true}` chunks. Created lazily on first // decode, dropped when a flushing decode ends the stream and in `Drop`. - codec: Cell>>, + codec: Cell>, // Read-only after construction (set in `constructor` before the JS wrapper // exists) — left bare. @@ -76,16 +75,6 @@ impl Default for TextDecoder { } } -impl Drop for TextDecoder { - fn drop(&mut self) { - if let Some(codec) = self.codec.get_mut().take() { - // SAFETY: `codec` was returned by `TextCodec::create` and has not - // been freed (the field is cleared whenever we destroy it). - unsafe { TextCodec::destroy(codec.as_ptr()) } - } - } -} - // pub const js = jsc.Codegen.JSTextDecoder; // pub const toJS / fromJS / fromJSDirect — provided by #[bun_jsc::JsClass] codegen. @@ -509,26 +498,22 @@ impl TextDecoder { // so reuse the one from the previous `{stream: true}` chunk. // Create it lazily on first use — matches WebKit's // `if (!m_codec) m_codec = newTextCodec(...)`. - let codec_ptr = match self.codec.get() { - Some(ptr) => ptr, + // Own the codec for the duration of the call and put it back + // below unless this decode flushes. The C++ `decode()` never + // calls into JS, so nothing observes the momentarily empty cell. + let codec = match self.codec.take() { + Some(codec) => codec, None => { - let Some(ptr) = TextCodec::create(encoding_name) else { + let Some(codec) = TextCodec::create(encoding_name) else { // Fallback to empty string if codec creation fails return Ok(ZigString::init(b"").to_js(global_this)); }; if !self.ignore_bom { - // `TextCodec` is an opaque ZST FFI handle (S008); - // `ptr` is live — safe via `opaque_deref_mut`. - bun_opaque::opaque_deref_mut(ptr.as_ptr()).strip_bom(); + codec.strip_bom(); } - self.codec.set(Some(ptr)); - ptr + codec } }; - // `TextCodec` is an opaque ZST FFI handle (S008); `codec_ptr` - // is live for this call — safe via `opaque_deref_mut`. The - // C++ `decode()` does not call back into JS, so no re-entrancy. - let codec = bun_opaque::opaque_deref_mut(codec_ptr.as_ptr()); // Decode the data let result = codec.decode(buffer_slice, FLUSH, self.fatal); @@ -543,10 +528,9 @@ impl TextDecoder { // reset on flush (e.g. `m_iso2022JPDecoderState`) would leak // into the next stream. if FLUSH { - self.codec.set(None); - // SAFETY: `codec_ptr` came from `TextCodec::create` above - // (or on an earlier chunk) and is freed exactly once here. - unsafe { TextCodec::destroy(codec_ptr.as_ptr()) }; + drop(codec); + } else { + self.codec.set(Some(codec)); } // Check for errors if fatal mode is enabled diff --git a/src/runtime/webcore/blob/copy_file.rs b/src/runtime/webcore/blob/copy_file.rs index 1a20daea6a7b..54588b48b99e 100644 --- a/src/runtime/webcore/blob/copy_file.rs +++ b/src/runtime/webcore/blob/copy_file.rs @@ -1570,10 +1570,10 @@ impl<'a> CopyFileWindows<'a> { pub fn throw(&mut self, err: bun_sys::Error) { let global_this = self.event_loop.global_ref(); - // `swap()` returns a `&mut JSPromise` into a GC-owned cell (not into - // `self`), but its lifetime is elided to `&mut self`. Decay to a raw pointer so - // borrowck doesn't tie it to `self` across `destroy` below. - let promise = JSPromise::opaque_mut(self.promise.swap()); + // `swap()` borrows a GC-owned cell (not `self`), but its lifetime is elided + // to `&mut self`. Decay to a raw pointer so borrowck doesn't tie it to `self` + // across `destroy` below. + let promise = JSPromise::opaque_ref(self.promise.swap()); let err_instance = err.to_js_with_async_stack(global_this, promise); // SAFETY: VM-owned event loop is valid for the process lifetime; `enter_scope` @@ -1662,7 +1662,7 @@ impl<'a> CopyFileWindows<'a> { let global_this = self.event_loop.global_ref(); // see `throw` — re-type the GC cell via the ZST opaque deref so it // outlives `destroy(self)` for borrowck. - let promise = JSPromise::opaque_mut(self.promise.swap()); + let promise = JSPromise::opaque_ref(self.promise.swap()); // SAFETY: VM-owned event loop is valid for the process lifetime; `enter_scope` // calls enter() now and exit() on drop. let _guard = unsafe { diff --git a/src/runtime/webcore/streams.rs b/src/runtime/webcore/streams.rs index f0c98cf81f62..fc62e7d5b392 100644 --- a/src/runtime/webcore/streams.rs +++ b/src/runtime/webcore/streams.rs @@ -804,16 +804,14 @@ impl StreamResult { let value = err.to_js(global_this); value.ensure_still_alive(); *result = StreamResult::Temporary(RawSlice::EMPTY); - // S008: `JSPromise` is an `opaque_ffi!` ZST — safe `*mut → &mut` - // deref. Fresh temp `&mut` is the sole borrow across this - // re-entrant call (no long-lived `&mut JSPromise` held). + // S008: `JSPromise` is an `opaque_ffi!` ZST — safe `*mut → &` deref. let _ = - JSPromise::opaque_mut(promise).reject_with_async_stack(global_this, Ok(value)); + JSPromise::opaque_ref(promise).reject_with_async_stack(global_this, Ok(value)); // TODO: properly propagate exception upwards } StreamResult::Done => { - // S008: see reject_with_async_stack above; fresh temp `&mut`. - let _ = JSPromise::opaque_mut(promise).resolve(global_this, JSValue::FALSE); + // S008: see reject_with_async_stack above. + let _ = JSPromise::opaque_ref(promise).resolve(global_this, JSValue::FALSE); // TODO: properly propagate exception upwards } _ => { @@ -821,8 +819,8 @@ impl StreamResult { Ok(v) => v, Err(err) => { *result = StreamResult::Temporary(RawSlice::EMPTY); - // S008: see reject_with_async_stack above; fresh temp `&mut`. - let _ = JSPromise::opaque_mut(promise).reject(global_this, Err(err)); + // S008: see reject_with_async_stack above. + let _ = JSPromise::opaque_ref(promise).reject(global_this, Err(err)); // TODO: properly propagate exception upwards vm.event_loop_ref().exit(); return; @@ -831,8 +829,8 @@ impl StreamResult { value.ensure_still_alive(); *result = StreamResult::Temporary(RawSlice::EMPTY); - // S008: see reject_with_async_stack above; fresh temp `&mut`. - let _ = JSPromise::opaque_mut(promise).resolve(global_this, value); + // S008: see reject_with_async_stack above. + let _ = JSPromise::opaque_ref(promise).resolve(global_this, value); // TODO: properly propagate exception upwards } } @@ -2014,9 +2012,9 @@ impl HTTPServerWritable { bun_core::scoped_log!(HTTPServerWritableLog, "flushPromise()"); let global_this = self.global_this(); - // S008: `JSPromise` is an `opaque_ffi!` ZST — safe `* → &`/`&mut` deref. + // S008: `JSPromise` is an `opaque_ffi!` ZST — safe `* → &` deref. JSPromise::opaque_ref(prom).to_js().unprotect(); - let result = JSPromise::opaque_mut(prom).resolve( + let result = JSPromise::opaque_ref(prom).resolve( global_this, JSValue::js_number(self.wrote.saturating_sub(self.wrote_at_start_of_flush) as f64), ); @@ -2501,8 +2499,8 @@ impl BufferAction { global: &JSGlobalObject, err: &StreamError, ) -> core::result::Result<(), jsc::JsTerminated> { - // S008: `JSPromise` is an `opaque_ffi!` ZST — safe `*mut → &mut` deref. - JSPromise::opaque_mut(self.swap()).reject(global, Ok(err.to_js(global))) + // S008: `JSPromise` is an `opaque_ffi!` ZST — safe `*mut → &` deref. + JSPromise::opaque_ref(self.swap()).reject(global, Ok(err.to_js(global))) } pub fn resolve( @@ -2510,8 +2508,8 @@ impl BufferAction { global: &JSGlobalObject, result: JSValue, ) -> core::result::Result<(), jsc::JsTerminated> { - // S008: `JSPromise` is an `opaque_ffi!` ZST — safe `*mut → &mut` deref. - JSPromise::opaque_mut(self.swap()).resolve(global, result) + // S008: `JSPromise` is an `opaque_ffi!` ZST — safe `*mut → &` deref. + JSPromise::opaque_ref(self.swap()).resolve(global, result) } pub fn value(&self) -> JSValue { diff --git a/src/sha_hmac/sha.rs b/src/sha_hmac/sha.rs index 7b8ae05ef75a..a91e0c18a2ad 100644 --- a/src/sha_hmac/sha.rs +++ b/src/sha_hmac/sha.rs @@ -14,8 +14,14 @@ use bun_boringssl_sys as boringssl_sys; // identity across crates (eliminating the cross-crate opaque-pointer casts). // ────────────────────────────────────────────────────────────────────────── pub mod ffi { + /// The C `ENGINE` object, **not** `bun_boringssl_sys`'s owning `ENGINE` + /// handle: `hash(.., engine)` below only borrows the VM-owned engine pointer + /// (`s3_signing::credentials` passes null), so nothing here releases a unit. + /// Kept under the name `ENGINE` so `bun_sha_hmac::sha::ffi::ENGINE` path + /// consumers need no edit. + pub use bun_boringssl_sys::sys::ENGINE; pub use bun_boringssl_sys::{ - ENGINE, EVP_Digest, EVP_DigestFinal, EVP_DigestInit, EVP_DigestUpdate, EVP_MD, EVP_MD_CTX, + EVP_Digest, EVP_DigestFinal, EVP_DigestInit, EVP_DigestUpdate, EVP_MD, EVP_MD_CTX, EVP_MD_CTX_cleanup, EVP_MD_CTX_init, EVP_blake2b256, EVP_blake2b512, EVP_md4, EVP_md5, EVP_md5_sha1, EVP_ripemd160, EVP_sha1, EVP_sha3_224, EVP_sha3_256, EVP_sha3_384, EVP_sha3_512, EVP_sha224, EVP_sha256, EVP_sha384, EVP_sha512, EVP_sha512_224, diff --git a/src/sql_jsc/mysql/MySQLConnection.rs b/src/sql_jsc/mysql/MySQLConnection.rs index 4e7e8d89c0dc..1a1b5a06b3b3 100644 --- a/src/sql_jsc/mysql/MySQLConnection.rs +++ b/src/sql_jsc/mysql/MySQLConnection.rs @@ -311,8 +311,8 @@ impl MySQLConnection { self.auth_data = Vec::new(); if let Some(s) = self.secure.take() { - // SAFETY: FFI — secure is an owned SSL_CTX* freed exactly once here - unsafe { bun_boringssl_sys::SSL_CTX_free(s) }; + // `secure` is an owned SSL_CTX ref, released exactly once here. + bun_boringssl_sys::SSL_CTX_free(SslCtx::opaque_ref(s)); } // _options_buf dropped at scope exit (Box<[u8]> frees via Drop) } diff --git a/src/sql_jsc/postgres/PostgresSQLConnection.rs b/src/sql_jsc/postgres/PostgresSQLConnection.rs index 2f6151107088..4d51b3205e62 100644 --- a/src/sql_jsc/postgres/PostgresSQLConnection.rs +++ b/src/sql_jsc/postgres/PostgresSQLConnection.rs @@ -1446,8 +1446,7 @@ impl PostgresSQLConnection { // tls_config dropped by Box drop below. if let Some(s) = (*this).secure { - // SSL_CTX_free on a valid SSL_CTX*. - BoringSSL::c::SSL_CTX_free(s); + BoringSSL::c::SSL_CTX_free(uws::SslCtx::opaque_ref(s)); } // Box-allocated in `call()`; ref_count is 0; reclaim. drop(bun_core::heap::take(this)); diff --git a/src/sql_jsc/shared/ConnectionCtorArgs.rs b/src/sql_jsc/shared/ConnectionCtorArgs.rs index 248234002ccc..86488984d2dd 100644 --- a/src/sql_jsc/shared/ConnectionCtorArgs.rs +++ b/src/sql_jsc/shared/ConnectionCtorArgs.rs @@ -42,8 +42,8 @@ pub(crate) type TlsGuard = scopeguard::ScopeGuard; pub(crate) fn guard_tls(secure: Option<*mut uws::SslCtx>, tls_config: SSLConfig) -> TlsGuard { fn free((secure, _tls_config): GuardState) { if let Some(s) = secure { - // SAFETY: `secure` holds one `ssl_ctx_cache` reference owned by the caller. - unsafe { bun_boringssl_sys::SSL_CTX_free(s) }; + // `secure` holds one `ssl_ctx_cache` reference owned by the caller. + bun_boringssl_sys::SSL_CTX_free(uws::SslCtx::opaque_ref(s)); } } scopeguard::guard((secure, tls_config), free as fn(GuardState)) diff --git a/src/tcc_sys/tcc.rs b/src/tcc_sys/tcc.rs index 8d2337567850..22ef40cce7ec 100644 --- a/src/tcc_sys/tcc.rs +++ b/src/tcc_sys/tcc.rs @@ -136,7 +136,7 @@ pub type SymbolCallback = unsafe extern "C" fn(ctx: *mut c_void, name: *const c_char, val: *const Symbol); bun_opaque::opaque_ffi! { - /// Opaque TinyCC compilation state. Always handled via `*mut State` / `&mut State`. + /// Opaque TinyCC compilation state. Always handled via `*mut State` / `&State`. pub struct State; } @@ -185,8 +185,7 @@ impl State { // SAFETY: p was returned by tcc_new and has not yet been deleted. unsafe { tcc_delete(p.as_ptr()) } }); - // SAFETY: state_ptr is valid and uniquely owned for the duration of this fn. - let state: &mut State = unsafe { &mut *state_ptr.as_ptr() }; + let state: &State = State::opaque_ref(state_ptr.as_ptr()); // setOutputType has side effects that are conditional on existing // options, so this must be called after setOptions @@ -232,14 +231,14 @@ impl State { } /// Set `CONFIG_TCCDIR` at runtime - pub fn set_lib_path(&mut self, path: &ZStr) { - // SAFETY: self is a valid *mut TCCState; path is NUL-terminated. - unsafe { tcc_set_lib_path(self, path.as_ptr()) } + pub fn set_lib_path(&self, path: &ZStr) { + // SAFETY: as_mut_ptr() yields a valid *mut TCCState; path is NUL-terminated. + unsafe { tcc_set_lib_path(self.as_mut_ptr(), path.as_ptr()) } } /// Set error/warning display callback pub fn set_error_func( - &mut self, + &self, error_opaque: Option<*mut Context>, error_func: ErrorFunc, ) { @@ -253,8 +252,8 @@ impl State { >(error_func) }); let opaque = error_opaque.map_or(core::ptr::null_mut(), |p| p.cast::()); - // SAFETY: self is a valid *mut TCCState. - unsafe { tcc_set_error_func(self, opaque, erased) } + // SAFETY: as_mut_ptr() yields a valid *mut TCCState. + unsafe { tcc_set_error_func(self.as_mut_ptr(), opaque, erased) } } // NOTE: get_error_func / get_error_opaque wrappers removed — the underlying @@ -262,9 +261,9 @@ impl State { // libtcc.h and would fail to link if referenced. /// Set options as from command line (multiple supported) - pub fn set_options(&mut self, str_: &ZStr) -> Result<(), Error> { - // SAFETY: self is a valid *mut TCCState; str_ is NUL-terminated. - if unsafe { tcc_set_options(self, str_.as_ptr()) } != 0 { + pub fn set_options(&self, str_: &ZStr) -> Result<(), Error> { + // SAFETY: as_mut_ptr() yields a valid *mut TCCState; str_ is NUL-terminated. + if unsafe { tcc_set_options(self.as_mut_ptr(), str_.as_ptr()) } != 0 { return Err(Error::InvalidOptions); } Ok(()) @@ -273,18 +272,18 @@ impl State { // ======================== Preprocessor ======================== /// Add include path - pub fn add_include_path(&mut self, pathname: &ZStr) -> Result<(), Error> { - // SAFETY: self is a valid *mut TCCState; pathname is NUL-terminated. - if unsafe { tcc_add_include_path(self, pathname.as_ptr()) } != 0 { + pub fn add_include_path(&self, pathname: &ZStr) -> Result<(), Error> { + // SAFETY: as_mut_ptr() yields a valid *mut TCCState; pathname is NUL-terminated. + if unsafe { tcc_add_include_path(self.as_mut_ptr(), pathname.as_ptr()) } != 0 { return Err(Error::InvalidIncludePath); } Ok(()) } /// Add in system include path - pub fn add_sys_include_path(&mut self, pathname: &ZStr) -> Result<(), Error> { - // SAFETY: self is a valid *mut TCCState; pathname is NUL-terminated. - if unsafe { tcc_add_sysinclude_path(self, pathname.as_ptr()) } != 0 { + pub fn add_sys_include_path(&self, pathname: &ZStr) -> Result<(), Error> { + // SAFETY: as_mut_ptr() yields a valid *mut TCCState; pathname is NUL-terminated. + if unsafe { tcc_add_sysinclude_path(self.as_mut_ptr(), pathname.as_ptr()) } != 0 { return Err(Error::InvalidIncludePath); } Ok(()) @@ -295,9 +294,9 @@ impl State { /// ```c /// #define sym value /// ``` - pub fn define_symbol(&mut self, sym: &ZStr, value: &ZStr) { - // SAFETY: self is a valid *mut TCCState; sym/value are NUL-terminated. - unsafe { tcc_define_symbol(self, sym.as_ptr(), value.as_ptr()) } + pub fn define_symbol(&self, sym: &ZStr, value: &ZStr) { + // SAFETY: as_mut_ptr() yields a valid *mut TCCState; sym/value are NUL-terminated. + unsafe { tcc_define_symbol(self.as_mut_ptr(), sym.as_ptr(), value.as_ptr()) } } /// Define multiple preprocessor symbols with integer values. @@ -306,7 +305,7 @@ impl State { /// ```ignore /// state.define_symbols(&[("foo", 1), ("baz", 42)]); /// ``` - pub fn define_symbols(&mut self, symbols: &[(&str, i64)]) { + pub fn define_symbols(&self, symbols: &[(&str, i64)]) { let mut buf = [0u8; 256]; for &(name, value) in symbols { // Copy the name into the stack buffer to NUL-terminate it for the C ABI. @@ -325,9 +324,9 @@ impl State { buf[val_end] = 0; let val_ptr = buf[val_off..].as_ptr().cast::(); - // SAFETY: self is a valid *mut TCCState; both buffer regions are NUL-terminated and - // outlive the FFI call (tcc_define_symbol copies its arguments). - unsafe { tcc_define_symbol(self, sym_ptr, val_ptr) } + // SAFETY: as_mut_ptr() yields a valid *mut TCCState; both buffer regions are + // NUL-terminated and outlive the FFI call (tcc_define_symbol copies its arguments). + unsafe { tcc_define_symbol(self.as_mut_ptr(), sym_ptr, val_ptr) } } } @@ -336,9 +335,9 @@ impl State { /// ```c /// #undef sym /// ``` - pub fn undefine_symbol(&mut self, sym: &ZStr) { - // SAFETY: self is a valid *mut TCCState; sym is NUL-terminated. - unsafe { tcc_undefine_symbol(self, sym.as_ptr()) } + pub fn undefine_symbol(&self, sym: &ZStr) { + // SAFETY: as_mut_ptr() yields a valid *mut TCCState; sym is NUL-terminated. + unsafe { tcc_undefine_symbol(self.as_mut_ptr(), sym.as_ptr()) } } // ======================== Compiling ======================== @@ -348,18 +347,18 @@ impl State { /// ## Errors /// - File not found /// - Syntax/formatting error - pub fn add_file(&mut self, filename: &ZStr) -> Result<(), Error> { - // SAFETY: self is a valid *mut TCCState; filename is NUL-terminated. - if unsafe { tcc_add_file(self, filename.as_ptr()) } != 0 { + pub fn add_file(&self, filename: &ZStr) -> Result<(), Error> { + // SAFETY: as_mut_ptr() yields a valid *mut TCCState; filename is NUL-terminated. + if unsafe { tcc_add_file(self.as_mut_ptr(), filename.as_ptr()) } != 0 { return Err(Error::CompileError); } Ok(()) } /// Compile a string containing a C source. - pub fn compile_string(&mut self, buf: &ZStr) -> Result<(), Error> { - // SAFETY: self is a valid *mut TCCState; buf is NUL-terminated. - if unsafe { tcc_compile_string(self, buf.as_ptr()) } != 0 { + pub fn compile_string(&self, buf: &ZStr) -> Result<(), Error> { + // SAFETY: as_mut_ptr() yields a valid *mut TCCState; buf is NUL-terminated. + if unsafe { tcc_compile_string(self.as_mut_ptr(), buf.as_ptr()) } != 0 { return Err(Error::CompileError); } Ok(()) @@ -368,27 +367,27 @@ impl State { // ======================== Linking Commands ======================== /// Set output type. MUST BE CALLED before any compilation - pub fn set_output_type(&mut self, output_type: OutputFormat) -> Result<(), Error> { - // SAFETY: self is a valid *mut TCCState. - if unsafe { tcc_set_output_type(self, output_type as c_int) } == -1 { + pub fn set_output_type(&self, output_type: OutputFormat) -> Result<(), Error> { + // SAFETY: as_mut_ptr() yields a valid *mut TCCState. + if unsafe { tcc_set_output_type(self.as_mut_ptr(), output_type as c_int) } == -1 { return Err(Error::InvalidOutputType); } Ok(()) } /// Add a library. Equivalent to `-Lpath` option - pub fn add_library_path(&mut self, pathname: &ZStr) -> Result<(), Error> { - // SAFETY: self is a valid *mut TCCState; pathname is NUL-terminated. - if unsafe { tcc_add_library_path(self, pathname.as_ptr()) } != 0 { + pub fn add_library_path(&self, pathname: &ZStr) -> Result<(), Error> { + // SAFETY: as_mut_ptr() yields a valid *mut TCCState; pathname is NUL-terminated. + if unsafe { tcc_add_library_path(self.as_mut_ptr(), pathname.as_ptr()) } != 0 { return Err(Error::InvalidLibraryPath); } Ok(()) } /// Add a library. The library name is the same as the argument of the `-l` option - pub fn add_library(&mut self, libraryname: &ZStr) -> Result<(), Error> { - // SAFETY: self is a valid *mut TCCState; libraryname is NUL-terminated. - if unsafe { tcc_add_library(self, libraryname.as_ptr()) } != 0 { + pub fn add_library(&self, libraryname: &ZStr) -> Result<(), Error> { + // SAFETY: as_mut_ptr() yields a valid *mut TCCState; libraryname is NUL-terminated. + if unsafe { tcc_add_library(self.as_mut_ptr(), libraryname.as_ptr()) } != 0 { return Err(Error::InvalidLibraryPath); } Ok(()) @@ -398,9 +397,10 @@ impl State { /// address and never dereferenced here; it must remain valid for any JIT'd /// code that calls it (same precondition as `add_symbols`). #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub fn add_symbol(&mut self, name: &ZStr, val: *const c_void) -> Result<(), Error> { - // SAFETY: self is a valid *mut TCCState; name is NUL-terminated; val is an opaque address. - if unsafe { tcc_add_symbol(self, name.as_ptr(), val) } != 0 { + pub fn add_symbol(&self, name: &ZStr, val: *const c_void) -> Result<(), Error> { + // SAFETY: as_mut_ptr() yields a valid *mut TCCState; name is NUL-terminated; val is an + // opaque address. + if unsafe { tcc_add_symbol(self.as_mut_ptr(), name.as_ptr(), val) } != 0 { return Err(Error::InvalidSymbol); } Ok(()) @@ -415,7 +415,7 @@ impl State { /// ("sub", sub as *const c_void), /// ])?; /// ``` - pub fn add_symbols(&mut self, symbols: &[(&str, *const c_void)]) -> Result<(), Error> { + pub fn add_symbols(&self, symbols: &[(&str, *const c_void)]) -> Result<(), Error> { // Copy each name into a stack buffer to NUL-terminate it for the C ABI. let mut buf = [0u8; 256]; for &(name, val) in symbols { @@ -423,9 +423,10 @@ impl State { debug_assert!(len < buf.len()); buf[..len].copy_from_slice(name.as_bytes()); buf[len] = 0; - // SAFETY: self is a valid *mut TCCState; buf[..=len] is NUL-terminated and outlives - // the FFI call (tcc_add_symbol copies the name); val is an opaque address. - if unsafe { tcc_add_symbol(self, buf.as_ptr().cast::(), val) } != 0 { + // SAFETY: as_mut_ptr() yields a valid *mut TCCState; buf[..=len] is NUL-terminated and + // outlives the FFI call (tcc_add_symbol copies the name); val is an opaque address. + if unsafe { tcc_add_symbol(self.as_mut_ptr(), buf.as_ptr().cast::(), val) } != 0 + { return Err(Error::InvalidSymbol); } } @@ -433,9 +434,9 @@ impl State { } /// Output an executable, library or object file. DO NOT call `relocate` before. - pub fn output_file(&mut self, filename: &ZStr) -> Result<(), Error> { - // SAFETY: self is a valid *mut TCCState; filename is NUL-terminated. - if unsafe { tcc_output_file(self, filename.as_ptr()) } == -1 { + pub fn output_file(&self, filename: &ZStr) -> Result<(), Error> { + // SAFETY: as_mut_ptr() yields a valid *mut TCCState; filename is NUL-terminated. + if unsafe { tcc_output_file(self.as_mut_ptr(), filename.as_ptr()) } == -1 { return Err(Error::OutputError); } Ok(()) @@ -443,18 +444,18 @@ impl State { /// Link and run `main()` function and return its value. DO NOT call `relocate` before. /// Returns the status code returned by the program's `main()` function. - pub fn run(&mut self, argc: c_int, argv: *const *const c_char) -> c_int { - // SAFETY: self is a valid *mut TCCState; argv points to argc NUL-terminated C strings. - // Cast const away to match the C ABI (tcc does not mutate argv). - unsafe { tcc_run(self, argc, argv as *mut *mut c_char) } + pub fn run(&self, argc: c_int, argv: *const *const c_char) -> c_int { + // SAFETY: as_mut_ptr() yields a valid *mut TCCState; argv points to argc NUL-terminated C + // strings. Cast const away to match the C ABI (tcc does not mutate argv). + unsafe { tcc_run(self.as_mut_ptr(), argc, argv as *mut *mut c_char) } } /// Do all relocations (needed before using `get_symbol`) /// Memory is allocated and managed internally by TinyCC. /// Returns Ok on success, error on failure. - pub fn relocate(&mut self) -> Result<(), Error> { - // SAFETY: self is a valid *mut TCCState. - let ret = unsafe { tcc_relocate(self) }; + pub fn relocate(&self) -> Result<(), Error> { + // SAFETY: as_mut_ptr() yields a valid *mut TCCState. + let ret = unsafe { tcc_relocate(self.as_mut_ptr()) }; if ret < 0 { return Err(Error::RelocationError); } @@ -462,16 +463,16 @@ impl State { } /// Return symbol value or NULL if not found - pub fn get_symbol(&mut self, name: &ZStr) -> Option> { - // SAFETY: self is a valid *mut TCCState; name is NUL-terminated. - NonNull::new(unsafe { tcc_get_symbol(self, name.as_ptr()) }.cast::()) + pub fn get_symbol(&self, name: &ZStr) -> Option> { + // SAFETY: as_mut_ptr() yields a valid *mut TCCState; name is NUL-terminated. + NonNull::new(unsafe { tcc_get_symbol(self.as_mut_ptr(), name.as_ptr()) }.cast::()) } /// Return symbol value or NULL if not found. /// `ctx` is forwarded opaquely to `symbol_cb`; it must be valid for every /// callback invocation (or null if `symbol_cb` ignores it). #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub fn list_symbols(&mut self, ctx: *mut c_void, symbol_cb: Option) { + pub fn list_symbols(&self, ctx: *mut c_void, symbol_cb: Option) { // SAFETY: SymbolCallback is ABI-identical to the extern's callback type // (`*const Symbol` vs `*const c_void` in the last param). let erased = symbol_cb.map(|f| unsafe { @@ -480,7 +481,7 @@ impl State { unsafe extern "C" fn(*mut c_void, *const c_char, *const c_void), >(f) }); - // SAFETY: self is a valid *mut TCCState. - unsafe { tcc_list_symbols(self, ctx, erased) } + // SAFETY: as_mut_ptr() yields a valid *mut TCCState. + unsafe { tcc_list_symbols(self.as_mut_ptr(), ctx, erased) } } } diff --git a/src/uws/lib.rs b/src/uws/lib.rs index a098654536d1..bc869849297c 100644 --- a/src/uws/lib.rs +++ b/src/uws/lib.rs @@ -34,11 +34,11 @@ pub use bun_uws_sys::response::State; pub use bun_uws_sys::{h3 as H3, quic, udp, vtable}; pub type Socket = us_socket_t; -/// Bare BoringSSL `SSL_CTX`. `SSL_CTX_up_ref`/`SSL_CTX_free` is the refcount; -/// policy (verify mode, reneg limits) is encoded on the SSL_CTX itself via -/// `us_ssl_ctx_from_options`, so there's no wrapper struct. `Option<*mut SslCtx>` -/// is what listen/connect/adopt take. -pub type SslCtx = bun_boringssl::c::SSL_CTX; +/// The BoringSSL `SSL_CTX` C object. `SSL_CTX_up_ref`/`SSL_CTX_free` is the +/// refcount; policy (verify mode, reneg limits) is encoded on the SSL_CTX itself +/// via `us_ssl_ctx_from_options`. `Option<*mut SslCtx>` is what +/// listen/connect/adopt take; the owning handle is `bun_boringssl::c::SSL_CTX`. +pub type SslCtx = bun_boringssl::c::sys::SSL_CTX; /// uWS C++ `WebSocketContext*`. Only ever produced by the /// upgrade-handler thunk and round-tripped to `uws_res_upgrade`; Rust never @@ -154,16 +154,22 @@ pub mod ssl_wrapper { // Re-export the canonical BoringSSL FFI surface; the lower-tier crate now // declares every symbol SSLWrapper needs, so the old local shim is gone. mod boring_sys { + // The C objects, not the owning handles: `SSLWrapper` stores the raw + // `SSL_CTX*` and releases it in `deinit`; the trust store returned by + // `us_get_shared_default_ca_store` is handed straight to the + // `SSL_set0_verify_cert_store` sink (a transfer, not a release), and the + // `X509_STORE_CTX*` in the verify callback is owned by BoringSSL. + pub(super) use bun_boringssl::c::sys::{SSL_CTX, X509_STORE, X509_STORE_CTX}; pub(super) use bun_boringssl::c::{ BIO_ctrl_pending, BIO_free, BIO_new, BIO_read, BIO_s_mem, BIO_set_mem_eof_return, - BIO_write, ERR_clear_error, SSL, SSL_CTX, SSL_CTX_free, SSL_CTX_get_verify_mode, - SSL_ERROR_SSL, SSL_ERROR_SYSCALL, SSL_ERROR_WANT_READ, SSL_ERROR_WANT_RENEGOTIATE, + BIO_write, ERR_clear_error, SSL, SSL_CTX_free, SSL_CTX_get_verify_mode, SSL_ERROR_SSL, + SSL_ERROR_SYSCALL, SSL_ERROR_WANT_READ, SSL_ERROR_WANT_RENEGOTIATE, SSL_ERROR_WANT_WRITE, SSL_ERROR_ZERO_RETURN, SSL_RECEIVED_SHUTDOWN, SSL_VERIFY_NONE, SSL_VERIFY_PEER, SSL_do_handshake, SSL_free, SSL_get_error, SSL_get_rbio, SSL_get_shutdown, SSL_get_wbio, SSL_is_init_finished, SSL_new, SSL_pending, SSL_read, SSL_renegotiate, SSL_set_accept_state, SSL_set_bio, SSL_set_connect_state, SSL_set_renegotiate_mode, SSL_set_verify, SSL_set0_verify_cert_store, SSL_shutdown, - SSL_write, X509_STORE, X509_STORE_CTX, ssl_renegotiate_explicit, ssl_renegotiate_never, + SSL_write, ssl_renegotiate_explicit, ssl_renegotiate_never, }; } @@ -383,9 +389,11 @@ pub mod ssl_wrapper { handlers: Handlers, ) -> Result { bun_boringssl::load(); - // SAFETY: ctx is a valid non-null SSL_CTX*; SSL_new returns null on OOM. - let ssl = NonNull::new(unsafe { boring_sys::SSL_new(ctx.as_ptr()) }) - .ok_or(InitError::OutOfMemory)?; + // SSL_new returns null on OOM. + let ssl = NonNull::new(boring_sys::SSL_new(boring_sys::SSL_CTX::opaque_ref( + ctx.as_ptr(), + ))) + .ok_or(InitError::OutOfMemory)?; // errdefer BoringSSL.SSL_free(ssl) — FFI cleanup on early return let ssl_guard = scopeguard::guard(ssl, |ssl| { // SAFETY: ssl was created by SSL_new above and is solely owned by this guard until disarmed. @@ -427,8 +435,9 @@ pub mod ssl_wrapper { // accident: net.ts forced `requestCert: true` after // `[buntls]` and `SSLConfig.fromJS` rebuilt the CTX with // roots from that.) - if boring_sys::SSL_CTX_get_verify_mode(ctx.as_ptr()) - == boring_sys::SSL_VERIFY_NONE + if boring_sys::SSL_CTX_get_verify_mode(boring_sys::SSL_CTX::opaque_ref( + ctx.as_ptr(), + )) == boring_sys::SSL_VERIFY_NONE { boring_sys::SSL_set_verify( ssl.as_ptr(), @@ -522,8 +531,7 @@ pub mod ssl_wrapper { // already freed inside create_ssl_context, so SSL_CTX_free is // sufficient on the error path. let ctx_guard = scopeguard::guard(ssl_ctx, |c| { - // SAFETY: ssl_ctx ref was just created by create_ssl_context and not yet adopted by init_with_ctx. - unsafe { boring_sys::SSL_CTX_free(c.as_ptr()) }; + boring_sys::SSL_CTX_free(boring_sys::SSL_CTX::opaque_ref(c.as_ptr())); }); let this = Self::init_with_ctx(ssl_ctx, is_client, handlers)?; let _ = scopeguard::ScopeGuard::into_inner(ctx_guard); @@ -799,8 +807,9 @@ pub mod ssl_wrapper { unsafe { boring_sys::SSL_free(ssl.as_ptr()) }; } if let Some(ctx) = self.ctx.take() { - // SAFETY: ctx ref was adopted in init/init_with_ctx; SSL_CTX_free decrements the C refcount and frees the SSL context and all the certificates when it hits zero. - unsafe { boring_sys::SSL_CTX_free(ctx.as_ptr()) }; + // The ref adopted in init/init_with_ctx; the C refcount frees the + // context and all its certificates when it hits zero. + boring_sys::SSL_CTX_free(boring_sys::SSL_CTX::opaque_ref(ctx.as_ptr())); } } diff --git a/src/uws_sys/App.rs b/src/uws_sys/App.rs index a1d0af41f224..f3813f2107a8 100644 --- a/src/uws_sys/App.rs +++ b/src/uws_sys/App.rs @@ -63,16 +63,16 @@ pub type NewApp = App; macro_rules! uws_app_route_methods { ($($name:ident => $cfn:ident),* $(,)?) => {$( pub fn $name( - &mut self, + &self, pattern: &[u8], handler: c::uws_method_handler, user_data: *mut c_void, ) { - // SAFETY: self is a valid app; pattern outlives the call (uWS copies it). + // SAFETY: pattern outlives the call (uWS copies it). unsafe { c::$cfn( Self::SSL_FLAG, - std::ptr::from_mut::(self).cast::(), + self.as_raw(), pattern.as_ptr(), pattern.len(), handler, @@ -87,21 +87,30 @@ impl App { pub const IS_SSL: bool = SSL; const SSL_FLAG: i32 = SSL as i32; - /// `&mut uws_app_s` view of self for `safe fn` shims. Both types are + /// Raw `uws_app_t*` with write provenance, derived through the `UnsafeCell`. + /// `App` is hand-written rather than `opaque_ffi!`-generated, so it needs its + /// own copy of the macro's accessor. `&Self` covers zero Rust-visible bytes, + /// so C++ mutating the app through the returned pointer cannot alias it. + #[inline(always)] + pub fn as_mut_ptr(&self) -> *mut Self { + self._p.get().cast::() + } + + /// `&uws_app_s` view of self for `safe fn` shims. Both types are /// `#[repr(C)]` opaque ZSTs with `UnsafeCell<[u8; 0]>`, so the cast is a /// no-op and the reference is ABI-identical to a non-null pointer. #[inline] - fn as_raw(&mut self) -> &mut uws_app_s { + fn as_raw(&self) -> &uws_app_s { // SAFETY: `App` and `uws_app_s` are layout-identical opaque ZSTs - // over the same C++ object; the borrow reborrows `&mut self`. - unsafe { &mut *std::ptr::from_mut::(self).cast::() } + // over the same C++ object; the borrow reborrows `&self`. + unsafe { &*std::ptr::from_ref::(self).cast::() } } - pub fn close(&mut self) { + pub fn close(&self) { c::uws_app_close(Self::SSL_FLAG, self.as_raw()) } - pub fn close_idle_connections(&mut self) { + pub fn close_idle_connections(&self) { c::uws_app_close_idle(Self::SSL_FLAG, self.as_raw()) } @@ -123,7 +132,7 @@ impl App { unsafe { c::uws_app_destroy(Self::SSL_FLAG, this.cast::()) } } - pub fn set_flags(&mut self, require_host_header: bool, use_strict_method_validation: bool) { + pub fn set_flags(&self, require_host_header: bool, use_strict_method_validation: bool) { c::uws_app_set_flags( Self::SSL_FLAG, self.as_raw(), @@ -132,26 +141,26 @@ impl App { ) } - pub fn set_max_http_header_size(&mut self, max_header_size: u64) { + pub fn set_max_http_header_size(&self, max_header_size: u64) { c::uws_app_set_max_http_header_size(Self::SSL_FLAG, self.as_raw(), max_header_size) } - pub fn clear_routes(&mut self) { + pub fn clear_routes(&self) { c::uws_app_clear_routes(Self::SSL_FLAG, self.as_raw()) } pub fn publish_with_options( - &mut self, + &self, topic: &[u8], message: &[u8], opcode: Opcode, compress: bool, ) -> SendStatus { - // SAFETY: self is a valid *mut uws_app_t; slices are valid for the call. + // SAFETY: slices are valid for the call. unsafe { c::uws_publish( SSL as i32, - std::ptr::from_mut::(self).cast::(), + self.as_raw(), topic.as_ptr(), topic.len(), message.as_ptr(), @@ -196,12 +205,12 @@ impl App { /// Alias matching uWS C++ `del()` naming (Rust `delete` is not reserved, but callers /// porting from uWS expect `del`). #[inline] - pub fn del(&mut self, pattern: &[u8], handler: c::uws_method_handler, user_data: *mut c_void) { + pub fn del(&self, pattern: &[u8], handler: c::uws_method_handler, user_data: *mut c_void) { self.delete(pattern, handler, user_data) } pub fn method( - &mut self, + &self, method_: Method, pattern: &[u8], handler: c::uws_method_handler, @@ -221,23 +230,17 @@ impl App { } } - pub fn domain(&mut self, pattern: &ZStr) { - // SAFETY: pattern is NUL-terminated; self is a valid app. - unsafe { - c::uws_app_domain( - Self::SSL_FLAG, - std::ptr::from_mut::(self).cast::(), - pattern.as_ptr().cast(), - ) - } + pub fn domain(&self, pattern: &ZStr) { + // SAFETY: pattern is NUL-terminated. + unsafe { c::uws_app_domain(Self::SSL_FLAG, self.as_raw(), pattern.as_ptr().cast()) } } - pub fn run(&mut self) { + pub fn run(&self) { c::uws_app_run(Self::SSL_FLAG, self.as_raw()) } pub fn listen( - &mut self, + &self, port: i32, handler: extern "C" fn(*mut UwsListenSocket, *mut c_void), user_data: *mut c_void, @@ -253,7 +256,7 @@ impl App { } pub fn on_client_error( - &mut self, + &self, handler: extern "C" fn(*mut c_void, c_int, *mut us_socket_t, u8, *mut u8, c_int), user_data: *mut c_void, ) { @@ -262,17 +265,17 @@ impl App { } pub fn listen_with_config( - &mut self, + &self, handler: c::uws_listen_handler, user_data: *mut c_void, config: c::uws_app_listen_config_t, ) { // Callers supply the C-ABI shim directly. - // SAFETY: self is a valid app; config.host (if non-null) is NUL-terminated and outlives the call. + // SAFETY: config.host (if non-null) is NUL-terminated and outlives the call. unsafe { c::uws_app_listen_with_config( Self::SSL_FLAG, - std::ptr::from_mut::(self).cast::(), + self.as_raw(), config.host, u16::try_from(config.port).expect("int cast"), config.options, @@ -283,18 +286,18 @@ impl App { } pub fn listen_on_unix_socket( - &mut self, + &self, handler: extern "C" fn(*mut UwsListenSocket, *const c_char, i32, *mut c_void), user_data: *mut c_void, domain_name: &ZStr, flags: i32, ) { // Callers supply the C-ABI shim directly. - // SAFETY: self is a valid app; domain_name is NUL-terminated. + // SAFETY: domain_name is NUL-terminated. unsafe { c::uws_app_listen_domain_with_options( Self::SSL_FLAG, - std::ptr::from_mut::(self).cast::(), + self.as_raw(), domain_name.as_ptr().cast(), domain_name.len(), flags, @@ -304,34 +307,29 @@ impl App { } } - pub fn constructor_failed(&mut self) -> bool { + pub fn constructor_failed(&self) -> bool { c::uws_constructor_failed(Self::SSL_FLAG, self.as_raw()) } - pub fn num_subscribers(&mut self, topic: &[u8]) -> u32 { - // SAFETY: self is a valid app; topic valid for the call. + pub fn num_subscribers(&self, topic: &[u8]) -> u32 { + // SAFETY: topic valid for the call. unsafe { - c::uws_num_subscribers( - Self::SSL_FLAG, - std::ptr::from_mut::(self).cast::(), - topic.as_ptr(), - topic.len(), - ) + c::uws_num_subscribers(Self::SSL_FLAG, self.as_raw(), topic.as_ptr(), topic.len()) } } pub fn publish( - &mut self, + &self, topic: &[u8], message: &[u8], opcode: Opcode, compress: bool, ) -> SendStatus { - // SAFETY: self is a valid app; slices valid for the call. + // SAFETY: slices valid for the call. unsafe { c::uws_publish( Self::SSL_FLAG, - std::ptr::from_mut::(self).cast::(), + self.as_raw(), topic.as_ptr(), topic.len(), message.as_ptr(), @@ -342,42 +340,32 @@ impl App { } } - pub fn get_native_handle(&mut self) -> *mut c_void { + pub fn get_native_handle(&self) -> *mut c_void { c::uws_get_native_handle(Self::SSL_FLAG, self.as_raw()) } - pub fn remove_server_name(&mut self, hostname_pattern: &core::ffi::CStr) { - // SAFETY: self is a valid app; hostname_pattern is NUL-terminated. + pub fn remove_server_name(&self, hostname_pattern: &core::ffi::CStr) { + // SAFETY: hostname_pattern is NUL-terminated. unsafe { - c::uws_remove_server_name( - Self::SSL_FLAG, - std::ptr::from_mut::(self).cast::(), - hostname_pattern.as_ptr(), - ) + c::uws_remove_server_name(Self::SSL_FLAG, self.as_raw(), hostname_pattern.as_ptr()) } } - pub fn add_server_name(&mut self, hostname_pattern: &core::ffi::CStr) { - // SAFETY: self is a valid app; hostname_pattern is NUL-terminated. - unsafe { - c::uws_add_server_name( - Self::SSL_FLAG, - std::ptr::from_mut::(self).cast::(), - hostname_pattern.as_ptr(), - ) - } + pub fn add_server_name(&self, hostname_pattern: &core::ffi::CStr) { + // SAFETY: hostname_pattern is NUL-terminated. + unsafe { c::uws_add_server_name(Self::SSL_FLAG, self.as_raw(), hostname_pattern.as_ptr()) } } pub fn add_server_name_with_options( - &mut self, + &self, hostname_pattern: &core::ffi::CStr, opts: &BunSocketContextOptions, ) -> Result<(), AddServerNameError> { - // SAFETY: self is a valid app; hostname_pattern is NUL-terminated. + // SAFETY: hostname_pattern is NUL-terminated. let rc = unsafe { c::uws_add_server_name_with_options( Self::SSL_FLAG, - std::ptr::from_mut::(self).cast::(), + self.as_raw(), hostname_pattern.as_ptr(), *opts, ) @@ -389,30 +377,27 @@ impl App { } pub fn missing_server_name( - &mut self, + &self, handler: c::uws_missing_server_handler, user_data: *mut c_void, ) { c::uws_missing_server_name(Self::SSL_FLAG, self.as_raw(), handler, user_data) } - pub fn filter(&mut self, handler: c::uws_filter_handler, user_data: *mut c_void) { + pub fn filter(&self, handler: c::uws_filter_handler, user_data: *mut c_void) { c::uws_filter(Self::SSL_FLAG, self.as_raw(), handler, user_data) } - pub fn ws( - &mut self, - pattern: &[u8], - ctx: *mut c_void, - id: usize, - behavior_: WebSocketBehavior, - ) { + pub fn ws(&self, pattern: &[u8], ctx: *mut c_void, id: usize, behavior_: WebSocketBehavior) { let behavior = behavior_; - // SAFETY: self is a valid app; pattern valid for the call; behavior is stack-local. + // SAFETY: pattern valid for the call; behavior is stack-local. `uws_ws` + // still spells its handle `*mut uws_app_t`; the app is an opaque ZST. unsafe { uws_ws( Self::SSL_FLAG, - std::ptr::from_mut::(self).cast::(), + std::ptr::from_ref::(self) + .cast::() + .cast_mut(), ctx, pattern.as_ptr(), pattern.len(), @@ -452,24 +437,25 @@ pub struct ListenSocket { impl ListenSocket { #[inline] - pub fn close(&mut self) { + pub fn close(&self) { // S008: ListenSocket is layout-identical to crate::ListenSocket - // (both ZST opaques) — safe deref via `opaque_deref_mut`. - bun_opaque::opaque_deref_mut(std::ptr::from_mut::(self).cast::()) - .close() + // (both ZST opaques) — safe deref via `opaque_deref`. + bun_opaque::opaque_deref(std::ptr::from_ref::(self).cast::()).close() } #[inline] - pub fn get_local_port(&mut self) -> i32 { + pub fn get_local_port(&self) -> i32 { // S008: opaque ZST cast as above. - bun_opaque::opaque_deref_mut(std::ptr::from_mut::(self).cast::()) + bun_opaque::opaque_deref(std::ptr::from_ref::(self).cast::()) .get_local_port() } - pub fn socket(&mut self) -> crate::socket::NewSocketHandler { + pub fn socket(&self) -> crate::socket::NewSocketHandler { // SAFETY: ListenSocket is layout-identical to us_socket_t on the C side // (a listen socket IS a us_socket_t). - crate::socket::NewSocketHandler::::from(std::ptr::from_mut::(self).cast()) + crate::socket::NewSocketHandler::::from( + std::ptr::from_ref::(self).cast_mut().cast(), + ) } } @@ -495,14 +481,14 @@ pub mod c { pub(crate) type uws_missing_server_handler = Option; unsafe extern "C" { - pub(crate) safe fn uws_app_close(ssl: i32, app: &mut uws_app_s); - pub(crate) safe fn uws_app_close_idle(ssl: i32, app: &mut uws_app_s); - // safe: `&mut uws_app_s` is ABI-identical to a non-null `*mut`; - // `handler`/`user_data` are stored opaquely (never dereferenced by the - // C++ shim itself) — no preconditions on this call. + pub(crate) safe fn uws_app_close(ssl: i32, app: &uws_app_s); + pub(crate) safe fn uws_app_close_idle(ssl: i32, app: &uws_app_s); + // safe: `&uws_app_s` is ABI-identical to a non-null `*mut` (the ZST is + // `!Freeze`, so no `noalias`/`readonly`); `handler`/`user_data` are stored + // opaquely (never dereferenced by the C++ shim itself). pub(crate) safe fn uws_app_set_on_clienterror( ssl: c_int, - app: &mut uws_app_s, + app: &uws_app_s, handler: extern "C" fn(*mut c_void, c_int, *mut us_socket_t, u8, *mut u8, c_int), user_data: *mut c_void, ); @@ -510,18 +496,18 @@ pub mod c { pub(crate) fn uws_app_destroy(ssl: i32, app: *mut uws_app_t); pub(crate) safe fn uws_app_set_flags( ssl: i32, - app: &mut uws_app_t, + app: &uws_app_t, require_host_header: bool, use_strict_method_validation: bool, ); pub(crate) safe fn uws_app_set_max_http_header_size( ssl: i32, - app: &mut uws_app_t, + app: &uws_app_t, max_header_size: u64, ); pub(crate) fn uws_app_get( ssl: i32, - app: *mut uws_app_t, + app: &uws_app_t, pattern: *const u8, pattern_len: usize, handler: uws_method_handler, @@ -529,7 +515,7 @@ pub mod c { ); pub(crate) fn uws_app_post( ssl: i32, - app: *mut uws_app_t, + app: &uws_app_t, pattern: *const u8, pattern_len: usize, handler: uws_method_handler, @@ -537,7 +523,7 @@ pub mod c { ); pub(crate) fn uws_app_options( ssl: i32, - app: *mut uws_app_t, + app: &uws_app_t, pattern: *const u8, pattern_len: usize, handler: uws_method_handler, @@ -545,7 +531,7 @@ pub mod c { ); pub(crate) fn uws_app_delete( ssl: i32, - app: *mut uws_app_t, + app: &uws_app_t, pattern: *const u8, pattern_len: usize, handler: uws_method_handler, @@ -553,7 +539,7 @@ pub mod c { ); pub(crate) fn uws_app_patch( ssl: i32, - app: *mut uws_app_t, + app: &uws_app_t, pattern: *const u8, pattern_len: usize, handler: uws_method_handler, @@ -561,7 +547,7 @@ pub mod c { ); pub(crate) fn uws_app_put( ssl: i32, - app: *mut uws_app_t, + app: &uws_app_t, pattern: *const u8, pattern_len: usize, handler: uws_method_handler, @@ -569,7 +555,7 @@ pub mod c { ); pub(crate) fn uws_app_head( ssl: i32, - app: *mut uws_app_t, + app: &uws_app_t, pattern: *const u8, pattern_len: usize, handler: uws_method_handler, @@ -577,7 +563,7 @@ pub mod c { ); pub(crate) fn uws_app_connect( ssl: i32, - app: *mut uws_app_t, + app: &uws_app_t, pattern: *const u8, pattern_len: usize, handler: uws_method_handler, @@ -585,7 +571,7 @@ pub mod c { ); pub(crate) fn uws_app_trace( ssl: i32, - app: *mut uws_app_t, + app: &uws_app_t, pattern: *const u8, pattern_len: usize, handler: uws_method_handler, @@ -593,42 +579,42 @@ pub mod c { ); pub(crate) fn uws_app_any( ssl: i32, - app: *mut uws_app_t, + app: &uws_app_t, pattern: *const u8, pattern_len: usize, handler: uws_method_handler, user_data: *mut c_void, ); - pub(crate) safe fn uws_app_run(ssl: i32, app: &mut uws_app_t); - pub(crate) fn uws_app_domain(ssl: i32, app: *mut uws_app_t, domain: *const c_char); + pub(crate) safe fn uws_app_run(ssl: i32, app: &uws_app_t); + pub(crate) fn uws_app_domain(ssl: i32, app: &uws_app_t, domain: *const c_char); // safe: handle-only + value `port`; `handler`/`user_data` are stored // opaquely — no preconditions on this call. pub(crate) safe fn uws_app_listen( ssl: i32, - app: &mut uws_app_t, + app: &uws_app_t, port: i32, handler: uws_listen_handler, user_data: *mut c_void, ); pub(crate) fn uws_app_listen_with_config( ssl: i32, - app: *mut uws_app_t, + app: &uws_app_t, host: *const c_char, port: u16, options: i32, handler: uws_listen_handler, user_data: *mut c_void, ); - pub(crate) safe fn uws_constructor_failed(ssl: i32, app: &mut uws_app_t) -> bool; + pub(crate) safe fn uws_constructor_failed(ssl: i32, app: &uws_app_t) -> bool; pub(crate) fn uws_num_subscribers( ssl: i32, - app: *mut uws_app_t, + app: &uws_app_t, topic: *const u8, topic_length: usize, ) -> c_uint; pub(crate) fn uws_publish( ssl: i32, - app: *mut uws_app_t, + app: &uws_app_t, topic: *const u8, topic_length: usize, message: *const u8, @@ -637,42 +623,42 @@ pub mod c { compress: bool, ) -> SendStatus; // safe: `uws_app_s` is an `opaque_ffi!` ZST (`UnsafeCell<[u8; 0]>`), so - // `&mut uws_app_s` is ABI-identical to the C `uws_app_t*` (non-null, + // `&uws_app_s` is ABI-identical to the C `uws_app_t*` (non-null, // no `noalias`/`readonly`). The C++ body only reads `app->getNativeHandle()` // — no preconditions beyond a live handle. - pub(crate) safe fn uws_get_native_handle(ssl: i32, app: &mut uws_app_s) -> *mut c_void; + pub(crate) safe fn uws_get_native_handle(ssl: i32, app: &uws_app_s) -> *mut c_void; pub(crate) fn uws_remove_server_name( ssl: i32, - app: *mut uws_app_t, + app: &uws_app_t, hostname_pattern: *const c_char, ); pub(crate) fn uws_add_server_name( ssl: i32, - app: *mut uws_app_t, + app: &uws_app_t, hostname_pattern: *const c_char, ); pub(crate) fn uws_add_server_name_with_options( ssl: i32, - app: *mut uws_app_t, + app: &uws_app_t, hostname_pattern: *const c_char, options: BunSocketContextOptions, ) -> i32; pub(crate) safe fn uws_missing_server_name( ssl: i32, - app: &mut uws_app_t, + app: &uws_app_t, handler: uws_missing_server_handler, user_data: *mut c_void, ); pub(crate) safe fn uws_filter( ssl: i32, - app: &mut uws_app_t, + app: &uws_app_t, handler: uws_filter_handler, user_data: *mut c_void, ); pub(crate) fn uws_app_listen_domain_with_options( ssl_flag: c_int, - app: *mut uws_app_t, + app: &uws_app_t, domain: *const c_char, pathlen: usize, flags: i32, @@ -680,7 +666,7 @@ pub mod c { user_data: *mut c_void, ); - pub(crate) safe fn uws_app_clear_routes(ssl_flag: c_int, app: &mut uws_app_t); + pub(crate) safe fn uws_app_clear_routes(ssl_flag: c_int, app: &uws_app_t); } #[repr(C)] diff --git a/src/uws_sys/ConnectingSocket.rs b/src/uws_sys/ConnectingSocket.rs index 5e64e57efb03..93ff99eb04f2 100644 --- a/src/uws_sys/ConnectingSocket.rs +++ b/src/uws_sys/ConnectingSocket.rs @@ -9,7 +9,7 @@ use crate::{Loop, SocketGroup, SocketKind}; bun_opaque::opaque_ffi! { pub struct ConnectingSocket; } impl ConnectingSocket { - pub fn close(&mut self) { + pub fn close(&self) { us_connecting_socket_close(self) } @@ -17,92 +17,89 @@ impl ConnectingSocket { /// shared by every socket it owns; /// materializing `&mut SocketGroup` here would alias with other sockets' /// borrows of the same group. - pub fn group(&mut self) -> *mut SocketGroup { + pub fn group(&self) -> *mut SocketGroup { us_connecting_socket_group(self) } - pub fn raw_group(&mut self) -> *mut SocketGroup { + pub fn raw_group(&self) -> *mut SocketGroup { self.group() } - pub fn kind(&mut self) -> SocketKind { + pub fn kind(&self) -> SocketKind { SocketKind::from_u8(us_connecting_socket_kind(self)) } /// Returns the owning `Loop`. Raw pointer because the loop is a shared /// singleton referenced by every group/socket/timer; /// materializing `&mut Loop` here would be aliased UB. - pub fn r#loop(&mut self) -> *mut Loop { + pub fn r#loop(&self) -> *mut Loop { us_connecting_socket_get_loop(self) } + /// `&mut self`: the returned `&mut T` aliases the socket's real trailing ext + /// storage, so the exclusive borrow — not the ZST receiver — is what keeps two + /// `&mut T` to that slot from coexisting. Caller asserts the slot was + /// sized/aligned for T at group creation. pub fn ext(&mut self) -> &mut T { - // SAFETY: the ext slot is per-socket trailing storage inside this - // allocation; `&mut self` guarantees exclusive access to it for the - // returned borrow's lifetime. Caller asserts the slot was sized/ - // aligned for T at group creation. + // SAFETY: `us_connecting_socket_ext` returns the per-socket ext slot. unsafe { &mut *us_connecting_socket_ext(self).cast::() } } - pub fn get_error(&mut self) -> i32 { + pub fn get_error(&self) -> i32 { us_connecting_socket_get_error(self) } /// Raw `getaddrinfo(3)` return code when the name lookup itself failed; /// 0 for a connect failure past name resolution. A different namespace /// from [`Self::get_error`] (errno). - pub fn get_dns_error(&mut self) -> i32 { + pub fn get_dns_error(&self) -> i32 { us_connecting_socket_get_dns_error(self) } - pub fn get_native_handle(&mut self) -> *mut c_void { + pub fn get_native_handle(&self) -> *mut c_void { us_connecting_socket_get_native_handle(self) } - pub fn is_closed(&mut self) -> bool { + pub fn is_closed(&self) -> bool { us_connecting_socket_is_closed(self) == 1 } - pub fn is_shutdown(&mut self) -> bool { + pub fn is_shutdown(&self) -> bool { us_connecting_socket_is_shut_down(self) == 1 } - pub fn long_timeout(&mut self, seconds: c_uint) { + pub fn long_timeout(&self, seconds: c_uint) { us_connecting_socket_long_timeout(self, seconds) } - pub fn shutdown(&mut self) { + pub fn shutdown(&self) { us_connecting_socket_shutdown(self) } - pub fn shutdown_read(&mut self) { + pub fn shutdown_read(&self) { us_connecting_socket_shutdown_read(self) } - pub fn timeout(&mut self, seconds: c_uint) { + pub fn timeout(&self, seconds: c_uint) { us_connecting_socket_timeout(self, seconds) } } -// All shims take only a non-null `us_connecting_socket_t*` plus value types. -// `ConnectingSocket` is `#[repr(C)]` with `UnsafeCell<[u8; 0]>`, so `&mut -// ConnectingSocket` is ABI-identical to a non-null pointer (no readonly/noalias -// attribute). Declaring the shims with reference params and `safe fn` moves the -// validity proof into the type signature. +// `ConnectingSocket` is `!Freeze`, so `&ConnectingSocket` carries neither +// `noalias` nor `readonly` and is ABI-identical to a non-null pointer. uSockets +// re-enters through the same pointer, so no shim may claim exclusivity. unsafe extern "C" { - pub(crate) safe fn us_connecting_socket_close(s: &mut ConnectingSocket); - pub(crate) safe fn us_connecting_socket_group(s: &mut ConnectingSocket) -> *mut SocketGroup; - pub(crate) safe fn us_connecting_socket_kind(s: &mut ConnectingSocket) -> u8; - pub(crate) safe fn us_connecting_socket_ext(s: &mut ConnectingSocket) -> *mut c_void; - pub(crate) safe fn us_connecting_socket_get_error(s: &mut ConnectingSocket) -> i32; - pub(crate) safe fn us_connecting_socket_get_dns_error(s: &mut ConnectingSocket) -> i32; - pub(crate) safe fn us_connecting_socket_get_native_handle( - s: &mut ConnectingSocket, - ) -> *mut c_void; - pub(crate) safe fn us_connecting_socket_is_closed(s: &mut ConnectingSocket) -> i32; - pub(crate) safe fn us_connecting_socket_is_shut_down(s: &mut ConnectingSocket) -> i32; - pub(crate) safe fn us_connecting_socket_long_timeout(s: &mut ConnectingSocket, seconds: c_uint); - pub(crate) safe fn us_connecting_socket_shutdown(s: &mut ConnectingSocket); - pub(crate) safe fn us_connecting_socket_shutdown_read(s: &mut ConnectingSocket); - pub(crate) safe fn us_connecting_socket_timeout(s: &mut ConnectingSocket, seconds: c_uint); - pub(crate) safe fn us_connecting_socket_get_loop(s: &mut ConnectingSocket) -> *mut Loop; + pub(crate) safe fn us_connecting_socket_close(s: &ConnectingSocket); + pub(crate) safe fn us_connecting_socket_group(s: &ConnectingSocket) -> *mut SocketGroup; + pub(crate) safe fn us_connecting_socket_kind(s: &ConnectingSocket) -> u8; + pub(crate) safe fn us_connecting_socket_ext(s: &ConnectingSocket) -> *mut c_void; + pub(crate) safe fn us_connecting_socket_get_error(s: &ConnectingSocket) -> i32; + pub(crate) safe fn us_connecting_socket_get_dns_error(s: &ConnectingSocket) -> i32; + pub(crate) safe fn us_connecting_socket_get_native_handle(s: &ConnectingSocket) -> *mut c_void; + pub(crate) safe fn us_connecting_socket_is_closed(s: &ConnectingSocket) -> i32; + pub(crate) safe fn us_connecting_socket_is_shut_down(s: &ConnectingSocket) -> i32; + pub(crate) safe fn us_connecting_socket_long_timeout(s: &ConnectingSocket, seconds: c_uint); + pub(crate) safe fn us_connecting_socket_shutdown(s: &ConnectingSocket); + pub(crate) safe fn us_connecting_socket_shutdown_read(s: &ConnectingSocket); + pub(crate) safe fn us_connecting_socket_timeout(s: &ConnectingSocket, seconds: c_uint); + pub(crate) safe fn us_connecting_socket_get_loop(s: &ConnectingSocket) -> *mut Loop; } diff --git a/src/uws_sys/ListenSocket.rs b/src/uws_sys/ListenSocket.rs index 8d6f3ef280b0..85f680230e8a 100644 --- a/src/uws_sys/ListenSocket.rs +++ b/src/uws_sys/ListenSocket.rs @@ -12,38 +12,34 @@ bun_opaque::opaque_ffi! { } impl ListenSocket { - pub fn close(&mut self) { + pub fn close(&self) { us_listen_socket_close(self) } - pub fn get_local_address<'a>( - &mut self, - buf: &'a mut [u8], - ) -> Result<&'a [u8], bun_core::Error> { + pub fn get_local_address<'a>(&self, buf: &'a mut [u8]) -> Result<&'a [u8], bun_core::Error> { self.get_socket().local_address(buf) } - pub fn get_local_port(&mut self) -> i32 { + pub fn get_local_port(&self) -> i32 { self.get_socket().local_port() } - pub fn get_socket(&mut self) -> &mut us_socket_t { + pub fn get_socket(&self) -> &us_socket_t { // S008: ListenSocket is layout-compatible with us_socket_t on the C side // (a listen socket IS a us_socket_t); both are `opaque_ffi!` ZSTs, so route - // the `*mut → &mut` pun through the const-asserted safe accessor. - us_socket_t::opaque_mut(std::ptr::from_mut::(self).cast::()) + // the `*const → &` pun through the const-asserted safe accessor. + us_socket_t::opaque_ref(std::ptr::from_ref::(self).cast::()) } - pub fn socket(&mut self) -> crate::socket::NewSocketHandler { + pub fn socket(&self) -> crate::socket::NewSocketHandler { // NewSocketHandler is local (crate::socket); no upward dep. - crate::socket::NewSocketHandler::::from(std::ptr::from_mut::( - self.get_socket(), - )) + // `as_mut_ptr` is the UnsafeCell route from `&self` to a write-provenance ptr. + crate::socket::NewSocketHandler::::from(self.as_mut_ptr().cast::()) } /// Group accepted sockets are linked into. The group is embedded in the /// owner and outlives every listen socket linked into it. - pub fn group(&mut self) -> ParentRef { + pub fn group(&self) -> ParentRef { // SAFETY: C returns a non-null group, with write provenance, that // outlives this listen socket. unsafe { ParentRef::from_raw_mut(us_listen_socket_group(self)) } @@ -55,7 +51,7 @@ impl ListenSocket { unsafe { &mut *us_listen_socket_ext(self).cast::() } } - pub fn fd(&mut self) -> Fd { + pub fn fd(&self) -> Fd { let raw = us_listen_socket_get_fd(self); // SOCKET → kind=system (mask bit 63); `from_native` would store the // raw bits verbatim and mis-tag `INVALID_SOCKET` (~0) as kind=uv. @@ -82,7 +78,7 @@ impl ListenSocket { /// a mutable pointer; accepting `&U` and const-casting would make that /// round-trip UB. pub fn add_server_name( - &mut self, + &self, hostname: &core::ffi::CStr, ssl_ctx: *mut SslCtx, user: *mut c_void, @@ -94,7 +90,7 @@ impl ListenSocket { unsafe { us_listen_socket_add_server_name(self, hostname.as_ptr(), ssl_ctx, user) == 0 } } - pub fn remove_server_name(&mut self, hostname: &core::ffi::CStr) { + pub fn remove_server_name(&self, hostname: &core::ffi::CStr) { // SAFETY: self and hostname are valid for the duration of the call. unsafe { us_listen_socket_remove_server_name(self, hostname.as_ptr()) } } @@ -103,10 +99,7 @@ impl ListenSocket { /// `hostname`, cast to `*mut T`. Returned as `NonNull` (not `&mut T`) /// because the pointee is caller-owned external storage — materializing a /// `&mut T` here could alias the caller's own live reference to it. - pub fn find_server_name_userdata( - &mut self, - hostname: &core::ffi::CStr, - ) -> Option> { + pub fn find_server_name_userdata(&self, hostname: &core::ffi::CStr) -> Option> { // SAFETY: self and hostname valid; caller guarantees the stored userdata // is a *T. let p = unsafe { us_listen_socket_find_server_name_userdata(self, hostname.as_ptr()) }; @@ -114,35 +107,34 @@ impl ListenSocket { } pub fn on_server_name( - &mut self, + &self, cb: extern "C" fn(*mut ListenSocket, *const c_char, *mut c_int, *mut c_void) -> *mut c_void, ) { us_listen_socket_on_server_name(self, cb) } } -// This file IS the *_sys crate, so externs live here. -// `ListenSocket` is `#[repr(C)]` with `UnsafeCell<[u8; 0]>`, so `&mut -// ListenSocket` is ABI-identical to a non-null pointer; value-typed shims are -// `safe fn`. Shims with nullable raw / ctx ptr stay unsafe. +// `ListenSocket` is `!Freeze`: `&ListenSocket` carries neither `noalias` nor +// `readonly` and is ABI-identical to a non-null pointer, so no shim needs `&mut`. +// Value-typed shims are `safe fn`; those with nullable raw/ctx ptrs stay unsafe. unsafe extern "C" { - safe fn us_listen_socket_close(ls: &mut ListenSocket); - safe fn us_listen_socket_group(ls: &mut ListenSocket) -> *mut SocketGroup; - safe fn us_listen_socket_ext(ls: &mut ListenSocket) -> *mut c_void; - safe fn us_listen_socket_get_fd(ls: &mut ListenSocket) -> LIBUS_SOCKET_DESCRIPTOR; + safe fn us_listen_socket_close(ls: &ListenSocket); + safe fn us_listen_socket_group(ls: &ListenSocket) -> *mut SocketGroup; + safe fn us_listen_socket_ext(ls: &ListenSocket) -> *mut c_void; + safe fn us_listen_socket_get_fd(ls: &ListenSocket) -> LIBUS_SOCKET_DESCRIPTOR; fn us_listen_socket_add_server_name( - ls: *mut ListenSocket, + ls: &ListenSocket, hostname: *const c_char, ssl_ctx: *mut SslCtx, user: *mut c_void, ) -> c_int; - fn us_listen_socket_remove_server_name(ls: *mut ListenSocket, hostname: *const c_char); + fn us_listen_socket_remove_server_name(ls: &ListenSocket, hostname: *const c_char); fn us_listen_socket_find_server_name_userdata( - ls: *mut ListenSocket, + ls: &ListenSocket, hostname: *const c_char, ) -> *mut c_void; safe fn us_listen_socket_on_server_name( - ls: &mut ListenSocket, + ls: &ListenSocket, cb: extern "C" fn(*mut ListenSocket, *const c_char, *mut c_int, *mut c_void) -> *mut c_void, ); } diff --git a/src/uws_sys/Request.rs b/src/uws_sys/Request.rs index 122d9484f6d0..a7d01dae77a1 100644 --- a/src/uws_sys/Request.rs +++ b/src/uws_sys/Request.rs @@ -34,7 +34,7 @@ impl AnyRequest { } pub fn set_yield(&mut self, y: bool) { match self { - Self::H1(r) => bun_opaque::opaque_deref_mut(*r).set_yield(y), + Self::H1(r) => bun_opaque::opaque_deref(*r).set_yield(y), Self::H3(r) => bun_opaque::opaque_deref_mut(*r).set_yield(y), } } @@ -52,7 +52,7 @@ impl Request { pub fn get_yield(&self) -> bool { c::uws_req_get_yield(self) } - pub fn set_yield(&mut self, yield_: bool) { + pub fn set_yield(&self, yield_: bool) { c::uws_req_set_yield(self, yield_) } pub fn url(&self) -> &[u8] { @@ -105,7 +105,7 @@ mod c { unsafe extern "C" { pub(super) safe fn uws_req_is_ancient(res: &Request) -> bool; pub(super) safe fn uws_req_get_yield(res: &Request) -> bool; - pub(super) safe fn uws_req_set_yield(res: &mut Request, yield_: bool); + pub(super) safe fn uws_req_set_yield(res: &Request, yield_: bool); // Out-param `dest` is a `&mut *const u8` (non-null, valid for write); the C // shim only stores a pointer into request-owned storage and returns its // length — no read-through-ptr precondition, so `safe fn`. diff --git a/src/uws_sys/Response.rs b/src/uws_sys/Response.rs index 6d51812af92a..4b134921a834 100644 --- a/src/uws_sys/Response.rs +++ b/src/uws_sys/Response.rs @@ -90,27 +90,24 @@ impl Response { } #[inline] - pub fn downcast(&mut self) -> *mut c::uws_res { - std::ptr::from_mut::(self).cast::() + pub fn downcast(&self) -> *mut c::uws_res { + self._p.get().cast::() } - /// `&mut uws_res` view of self for `safe fn` shims. Both types are - /// `#[repr(C)]` opaque ZSTs with `UnsafeCell<[u8; 0]>`, so the cast is a - /// no-op and the reference is ABI-identical to the non-null pointer the C - /// shim expects. + /// `&mut uws_res` view of self for `safe fn` shims. `_p` sits at offset 0 + /// of this `#[repr(C)]` ZST, so `UnsafeCell::get` yields self's own address + /// with write provenance — the sanctioned interior-mutability route. #[inline] - fn as_raw(&mut self) -> &mut c::uws_res { - // SAFETY: `Response` and `c::uws_res` are layout-identical opaque - // ZSTs over the same C++ object; the borrow reborrows `&mut self`. - unsafe { &mut *std::ptr::from_mut::(self).cast::() } + fn as_raw(&self) -> &mut c::uws_res { + c::uws_res::opaque_mut(self.downcast()) } #[inline] - pub fn downcast_socket(&mut self) -> *mut us_socket_t { - std::ptr::from_mut::(self).cast::() + pub fn downcast_socket(&self) -> *mut us_socket_t { + self._p.get().cast::() } - pub fn end(&mut self, data: &[u8], close_connection: bool) { + pub fn end(&self, data: &[u8], close_connection: bool) { // SAFETY: self is a live opaque uws_res handle owned by uWS; FFI call has no extra preconditions. unsafe { c::uws_res_end( @@ -123,7 +120,7 @@ impl Response { } } - pub fn try_end(&mut self, data: &[u8], total: usize, close_: bool) -> bool { + pub fn try_end(&self, data: &[u8], total: usize, close_: bool) -> bool { // SAFETY: self is a live opaque uws_res handle owned by uWS; FFI call has no extra preconditions. unsafe { c::uws_res_try_end( @@ -137,55 +134,54 @@ impl Response { } } - pub fn get_socket_data(&mut self) -> *mut c_void { + pub fn get_socket_data(&self) -> *mut c_void { c::uws_res_get_socket_data(Self::ssl_flag(), self.as_raw()).cast() } - pub fn is_connect_request(&mut self) -> bool { + pub fn is_connect_request(&self) -> bool { c::uws_res_is_connect_request(Self::ssl_flag(), self.as_raw()) } - pub fn flush_headers(&mut self, flush_immediately: bool) { + pub fn flush_headers(&self, flush_immediately: bool) { c::uws_res_flush_headers(Self::ssl_flag(), self.as_raw(), flush_immediately) } - pub fn is_corked(&mut self) -> bool { + pub fn is_corked(&self) -> bool { c::uws_res_is_corked(Self::ssl_flag(), self.as_raw()) } pub fn state(&self) -> State { - // SAFETY: `Response` and `c::uws_res` are layout-identical opaque - // ZSTs (both `UnsafeCell<[u8; 0]>`); the reborrow is a no-op cast. - c::uws_res_state(Self::ssl_flag() as c_int, unsafe { - &*std::ptr::from_ref::(self).cast::() - }) + c::uws_res_state( + Self::ssl_flag() as c_int, + c::uws_res::opaque_ref(self.downcast()), + ) } pub fn should_close_connection(&self) -> bool { self.state().is_http_connection_close() } - pub fn prepare_for_sendfile(&mut self) { + pub fn prepare_for_sendfile(&self) { c::uws_res_prepare_for_sendfile(Self::ssl_flag(), self.as_raw()) } - pub fn uncork(&mut self) { + pub fn uncork(&self) { c::uws_res_uncork(Self::ssl_flag(), self.as_raw()) } - pub fn pause(&mut self) { + pub fn pause(&self) { c::uws_res_pause(Self::ssl_flag(), self.as_raw()) } - pub fn resume_(&mut self) { + pub fn resume_(&self) { c::uws_res_resume(Self::ssl_flag(), self.as_raw()) } - pub fn write_continue(&mut self) { + pub fn write_continue(&self) { c::uws_res_write_continue(Self::ssl_flag(), self.as_raw()) } - pub fn write_status(&mut self, status: &[u8]) { + pub fn write_status(&self, status: &[u8]) { // SAFETY: self is a live opaque uws_res handle owned by uWS; FFI call has no extra preconditions. unsafe { c::uws_res_write_status( @@ -197,7 +193,7 @@ impl Response { } } - pub fn write_header(&mut self, key: &[u8], value: &[u8]) { + pub fn write_header(&self, key: &[u8], value: &[u8]) { // SAFETY: self is a live opaque uws_res handle owned by uWS; FFI call has no extra preconditions. unsafe { c::uws_res_write_header( @@ -211,7 +207,7 @@ impl Response { } } - pub fn write_header_int(&mut self, key: &[u8], value: u64) { + pub fn write_header_int(&self, key: &[u8], value: u64) { // SAFETY: self is a live opaque uws_res handle owned by uWS; FFI call has no extra preconditions. unsafe { c::uws_res_write_header_int( @@ -224,11 +220,11 @@ impl Response { } } - pub fn end_without_body(&mut self, close_connection: bool) { + pub fn end_without_body(&self, close_connection: bool) { c::uws_res_end_without_body(Self::ssl_flag(), self.as_raw(), close_connection) } - pub fn end_send_file(&mut self, write_offset: u64, close_connection: bool) { + pub fn end_send_file(&self, write_offset: u64, close_connection: bool) { c::uws_res_end_sendfile( Self::ssl_flag(), self.as_raw(), @@ -237,19 +233,19 @@ impl Response { ) } - pub fn timeout(&mut self, seconds: u8) { + pub fn timeout(&self, seconds: u8) { c::uws_res_timeout(Self::ssl_flag(), self.as_raw(), seconds) } - pub fn reset_timeout(&mut self) { + pub fn reset_timeout(&self) { c::uws_res_reset_timeout(Self::ssl_flag(), self.as_raw()) } - pub fn get_buffered_amount(&mut self) -> u64 { + pub fn get_buffered_amount(&self) -> u64 { c::uws_res_get_buffered_amount(Self::ssl_flag(), self.as_raw()) } - pub fn write(&mut self, data: &[u8]) -> WriteResult { + pub fn write(&self, data: &[u8]) -> WriteResult { let mut len: usize = data.len(); // SAFETY: self is a live opaque uws_res handle owned by uWS; FFI call has no extra preconditions. match unsafe { @@ -265,11 +261,11 @@ impl Response { } } - pub fn get_write_offset(&mut self) -> u64 { + pub fn get_write_offset(&self) -> u64 { c::uws_res_get_write_offset(Self::ssl_flag(), self.as_raw()) } - pub fn override_write_offset(&mut self, offset: T) + pub fn override_write_offset(&self, offset: T) where u64: TryFrom, >::Error: core::fmt::Debug, @@ -281,19 +277,19 @@ impl Response { ) } - pub fn has_responded(&mut self) -> bool { + pub fn has_responded(&self) -> bool { c::uws_res_has_responded(Self::ssl_flag(), self.as_raw()) } - pub fn mark_wrote_content_length_header(&mut self) { + pub fn mark_wrote_content_length_header(&self) { c::uws_res_mark_wrote_content_length_header(Self::ssl_flag(), self.as_raw()) } - pub fn write_mark(&mut self) { + pub fn write_mark(&self) { c::uws_res_write_mark(Self::ssl_flag(), self.as_raw()) } - pub fn get_native_handle(&mut self) -> Fd { + pub fn get_native_handle(&self) -> Fd { #[cfg(windows)] { // on windows uSockets exposes SOCKET (uintptr-sized) as a pointer @@ -315,7 +311,7 @@ impl Response { } } - pub fn get_remote_address_as_text(&mut self) -> Option<&[u8]> { + pub fn get_remote_address_as_text(&self) -> Option<&[u8]> { let mut buf: *const u8 = core::ptr::null(); let size = c::uws_res_get_remote_address_as_text(Self::ssl_flag(), self.as_raw(), &mut buf); if size > 0 { @@ -326,7 +322,7 @@ impl Response { } } - pub fn get_remote_socket_info(&mut self) -> Option { + pub fn get_remote_socket_info(&self) -> Option { let mut ip_ptr: *const u8 = core::ptr::null(); let mut port: i32 = 0; let mut is_ipv6: bool = false; @@ -354,7 +350,7 @@ impl Response { /// zero-sized type (function item or capture-less closure): the trampoline /// is monomorphized over `H` and conjures the ZST inside, so the user /// handler is baked in with no runtime storage. - pub fn on_writable(&mut self, _handler: H, user_data: *mut U) + pub fn on_writable(&self, _handler: H, user_data: *mut U) where H: Fn(*mut U, u64, &mut Response) -> bool + Copy + 'static, { @@ -390,18 +386,18 @@ impl Response { ); } - pub fn clear_on_writable(&mut self) { + pub fn clear_on_writable(&self) { c::uws_res_clear_on_writable(Self::ssl_flag(), self.as_raw()) } #[inline] - pub fn mark_needs_more(&mut self) { + pub fn mark_needs_more(&self) { if !SSL { c::us_socket_mark_needs_more_not_ssl(self.as_raw()) } } - pub fn on_aborted(&mut self, _handler: H, optional_data: *mut U) + pub fn on_aborted(&self, _handler: H, optional_data: *mut U) where H: Fn(*mut U, &mut Response) + Copy + 'static, { @@ -432,11 +428,11 @@ impl Response { ); } - pub fn clear_aborted(&mut self) { + pub fn clear_aborted(&self) { c::uws_res_on_aborted(Self::ssl_flag(), self.as_raw(), None, core::ptr::null_mut()) } - pub fn on_timeout(&mut self, _handler: H, optional_data: *mut U) + pub fn on_timeout(&self, _handler: H, optional_data: *mut U) where H: Fn(*mut U, &mut Response) + Copy + 'static, { @@ -467,15 +463,15 @@ impl Response { ); } - pub fn clear_timeout(&mut self) { + pub fn clear_timeout(&self) { c::uws_res_on_timeout(Self::ssl_flag(), self.as_raw(), None, core::ptr::null_mut()) } - pub fn clear_on_data(&mut self) { + pub fn clear_on_data(&self) { c::uws_res_on_data(Self::ssl_flag(), self.as_raw(), None, core::ptr::null_mut()) } - pub fn on_data(&mut self, _handler: H, optional_data: *mut U) + pub fn on_data(&self, _handler: H, optional_data: *mut U) where H: Fn(*mut U, &mut Response, &[u8], bool) + Copy + 'static, { @@ -513,12 +509,12 @@ impl Response { ); } - pub fn end_stream(&mut self, close_connection: bool) { + pub fn end_stream(&self, close_connection: bool) { c::uws_res_end_stream(Self::ssl_flag(), self.as_raw(), close_connection) } /// Run `handler` while the response is corked. - pub fn corked(&mut self, f: F) { + pub fn corked(&self, f: F) { // Safe fn item: nested local thunk, only coerced to the C-ABI // fn-pointer type passed to C; body wraps its raw-ptr op explicitly. extern "C" fn handle(user_data: *mut c_void) { @@ -536,7 +532,7 @@ impl Response { ); } - pub fn run_corked_with_type(&mut self, handler: fn(*mut U), optional_data: *mut U) { + pub fn run_corked_with_type(&self, handler: fn(*mut U), optional_data: *mut U) { // cork is synchronous, so we can stack-allocate the (handler, data) pair // and recover it inside the trampoline. type Ctx = (fn(*mut U), *mut U); @@ -558,7 +554,7 @@ impl Response { } pub fn upgrade( - &mut self, + &self, data: *mut D, sec_web_socket_key: &[u8], sec_web_socket_protocol: &[u8], diff --git a/src/uws_sys/SocketContext.rs b/src/uws_sys/SocketContext.rs index 01c214b0b098..479030893cf2 100644 --- a/src/uws_sys/SocketContext.rs +++ b/src/uws_sys/SocketContext.rs @@ -6,7 +6,7 @@ use core::ffi::{c_char, c_long}; use core::ptr; -use bun_boringssl_sys::SSL_CTX; +use bun_boringssl_sys::sys::SSL_CTX; use crate::create_bun_socket_error_t; diff --git a/src/uws_sys/WebSocket.rs b/src/uws_sys/WebSocket.rs index 0517c363c2ac..6654dba34948 100644 --- a/src/uws_sys/WebSocket.rs +++ b/src/uws_sys/WebSocket.rs @@ -204,7 +204,7 @@ impl NewWebSocket { bun_opaque::opaque_ffi! { pub struct RawWebSocket; } impl RawWebSocket { - pub fn memory_cost(&mut self, ssl_flag: i32) -> usize { + pub fn memory_cost(&self, ssl_flag: i32) -> usize { c::uws_ws_memory_cost(ssl_flag, self) } @@ -213,8 +213,10 @@ impl RawWebSocket { /// Equivalent to: /// /// (struct us_socket_t *)socket - pub fn as_socket(&mut self) -> *mut Socket { - std::ptr::from_mut::(self).cast::() + pub fn as_socket(&self) -> *mut Socket { + std::ptr::from_ref::(self) + .cast::() + .cast_mut() } } @@ -710,7 +712,7 @@ pub mod c { // is the socket itself (plus value types) are `safe fn`; (ptr,len) shims // and out-param shims stay unsafe. unsafe extern "C" { - pub(crate) safe fn uws_ws_memory_cost(ssl: i32, ws: &mut RawWebSocket) -> usize; + pub(crate) safe fn uws_ws_memory_cost(ssl: i32, ws: &RawWebSocket) -> usize; pub(crate) fn uws_ws( ssl: i32, app: *mut uws_app_t, diff --git a/src/uws_sys/h3.rs b/src/uws_sys/h3.rs index 1e454c926108..2cb14bb641af 100644 --- a/src/uws_sys/h3.rs +++ b/src/uws_sys/h3.rs @@ -17,13 +17,13 @@ use crate::thunk; bun_opaque::opaque_ffi! { pub struct ListenSocket; } impl ListenSocket { - pub fn close(&mut self) { + pub fn close(&self) { c::uws_h3_listen_socket_close(self) } - pub fn get_local_port(&mut self) -> i32 { + pub fn get_local_port(&self) -> i32 { c::uws_h3_listen_socket_port(self) } - pub fn get_local_address<'a>(&mut self, buf: &'a mut [u8]) -> Option<&'a [u8]> { + pub fn get_local_address<'a>(&self, buf: &'a mut [u8]) -> Option<&'a [u8]> { // SAFETY: self is a live FFI handle; buf ptr/len valid for write let n = unsafe { c::uws_h3_listen_socket_local_address( @@ -49,25 +49,25 @@ impl Request { pub fn is_ancient(&self) -> bool { false } - pub fn get_yield(&mut self) -> bool { + pub fn get_yield(&self) -> bool { c::uws_h3_req_get_yield(self) } - pub fn set_yield(&mut self, y: bool) { + pub fn set_yield(&self, y: bool) { c::uws_h3_req_set_yield(self, y) } - pub fn url(&mut self) -> &[u8] { + pub fn url(&self) -> &[u8] { let mut p: *const u8 = ptr::null(); let n = c::uws_h3_req_get_url(self, &mut p); // SAFETY: uws returns a pointer+len pair valid for the lifetime of the request unsafe { bun_core::ffi::slice(p, n) } } - pub fn method(&mut self) -> &[u8] { + pub fn method(&self) -> &[u8] { let mut p: *const u8 = ptr::null(); let n = c::uws_h3_req_get_method(self, &mut p); // SAFETY: uws returns a pointer+len pair valid for the lifetime of the request unsafe { bun_core::ffi::slice(p, n) } } - pub fn header(&mut self, name: &[u8]) -> Option<&[u8]> { + pub fn header(&self, name: &[u8]) -> Option<&[u8]> { let mut p: *const u8 = ptr::null(); // SAFETY: self is a live FFI handle; name ptr/len valid for read; out-ptr is a valid local let n = unsafe { c::uws_h3_req_get_header(self, name.as_ptr(), name.len(), &raw mut p) }; @@ -78,14 +78,14 @@ impl Request { Some(unsafe { bun_core::ffi::slice(p, n) }) } } - pub fn query(&mut self, name: &[u8]) -> &[u8] { + pub fn query(&self, name: &[u8]) -> &[u8] { let mut p: *const u8 = ptr::null(); // SAFETY: self is a live FFI handle; name ptr/len valid for read; out-ptr is a valid local let n = unsafe { c::uws_h3_req_get_query(self, name.as_ptr(), name.len(), &raw mut p) }; // SAFETY: uws returns a pointer+len pair valid for the lifetime of the request unsafe { bun_core::ffi::slice(p, n) } } - pub fn parameter(&mut self, idx: u16) -> &[u8] { + pub fn parameter(&self, idx: u16) -> &[u8] { let mut p: *const u8 = ptr::null(); let n = c::uws_h3_req_get_parameter(self, idx, &mut p); // SAFETY: uws returns a pointer+len pair valid for the lifetime of the request @@ -97,7 +97,7 @@ impl Request { /// zero-sized type (function item or capture-less closure): the trampoline /// is monomorphized over `H` and conjures the ZST inside, so the user /// handler is baked in with no runtime storage. - pub fn for_each_header(&mut self, _cb: H, ctx: *mut Ctx) + pub fn for_each_header(&self, _cb: H, ctx: *mut Ctx) where H: Fn(&mut Ctx, &[u8], &[u8]) + Copy + 'static, { @@ -133,24 +133,24 @@ impl Request { bun_opaque::opaque_ffi! { pub struct Response; } impl Response { - pub fn end(&mut self, data: &[u8], close_connection: bool) { + pub fn end(&self, data: &[u8], close_connection: bool) { // SAFETY: self is a live FFI handle; data ptr/len valid for read unsafe { c::uws_h3_res_end(self, data.as_ptr(), data.len(), close_connection) } } - pub fn try_end(&mut self, data: &[u8], total: usize, close_connection: bool) -> bool { + pub fn try_end(&self, data: &[u8], total: usize, close_connection: bool) -> bool { // SAFETY: self is a live FFI handle; data ptr/len valid for read unsafe { c::uws_h3_res_try_end(self, data.as_ptr(), data.len(), total, close_connection) } } - pub fn end_without_body(&mut self, close_connection: bool) { + pub fn end_without_body(&self, close_connection: bool) { c::uws_h3_res_end_without_body(self, close_connection) } - pub fn end_stream(&mut self, close_connection: bool) { + pub fn end_stream(&self, close_connection: bool) { c::uws_h3_res_end_stream(self, close_connection) } - pub fn end_send_file(&mut self, write_offset: u64, close_connection: bool) { + pub fn end_send_file(&self, write_offset: u64, close_connection: bool) { c::uws_h3_res_end_sendfile(self, write_offset, close_connection) } - pub fn write(&mut self, data: &[u8]) -> WriteResult { + pub fn write(&self, data: &[u8]) -> WriteResult { let mut len: usize = data.len(); // SAFETY: self is a live FFI handle; data ptr valid for read; len out-ptr is a valid local if unsafe { c::uws_h3_res_write(self, data.as_ptr(), &raw mut len) } { @@ -159,79 +159,79 @@ impl Response { WriteResult::Backpressure(len) } } - pub fn write_status(&mut self, status: &[u8]) { + pub fn write_status(&self, status: &[u8]) { // SAFETY: self is a live FFI handle; status ptr/len valid for read unsafe { c::uws_h3_res_write_status(self, status.as_ptr(), status.len()) } } - pub fn write_header(&mut self, key: &[u8], value: &[u8]) { + pub fn write_header(&self, key: &[u8], value: &[u8]) { // SAFETY: self is a live FFI handle; key/value ptr+len valid for read unsafe { c::uws_h3_res_write_header(self, key.as_ptr(), key.len(), value.as_ptr(), value.len()) } } - pub fn write_header_int(&mut self, key: &[u8], value: u64) { + pub fn write_header_int(&self, key: &[u8], value: u64) { // SAFETY: self is a live FFI handle; key ptr/len valid for read unsafe { c::uws_h3_res_write_header_int(self, key.as_ptr(), key.len(), value) } } - pub fn write_mark(&mut self) { + pub fn write_mark(&self) { c::uws_h3_res_write_mark(self) } - pub fn mark_wrote_content_length_header(&mut self) { + pub fn mark_wrote_content_length_header(&self) { c::uws_h3_res_mark_wrote_content_length_header(self) } - pub fn write_continue(&mut self) { + pub fn write_continue(&self) { c::uws_h3_res_write_continue(self) } - pub fn flush_headers(&mut self, immediate: bool) { + pub fn flush_headers(&self, immediate: bool) { c::uws_h3_res_flush_headers(self, immediate) } - pub fn pause(&mut self) { + pub fn pause(&self) { c::uws_h3_res_pause(self) } - pub fn resume_(&mut self) { + pub fn resume_(&self) { c::uws_h3_res_resume(self) } #[inline] - pub fn resume(&mut self) { + pub fn resume(&self) { self.resume_() } - pub fn timeout(&mut self, seconds: u8) { + pub fn timeout(&self, seconds: u8) { c::uws_h3_res_timeout(self, seconds) } - pub fn reset_timeout(&mut self) { + pub fn reset_timeout(&self) { c::uws_h3_res_reset_timeout(self) } - pub fn get_write_offset(&mut self) -> u64 { + pub fn get_write_offset(&self) -> u64 { c::uws_h3_res_get_write_offset(self) } - pub fn override_write_offset(&mut self, off: u64) { + pub fn override_write_offset(&self, off: u64) { c::uws_h3_res_override_write_offset(self, off) } - pub fn get_buffered_amount(&mut self) -> u64 { + pub fn get_buffered_amount(&self) -> u64 { c::uws_h3_res_get_buffered_amount(self) } - pub fn has_responded(&mut self) -> bool { + pub fn has_responded(&self) -> bool { c::uws_h3_res_has_responded(self) } - pub fn state(&mut self) -> State { + pub fn state(&self) -> State { c::uws_h3_res_state(self) } - pub fn should_close_connection(&mut self) -> bool { + pub fn should_close_connection(&self) -> bool { self.state().is_http_connection_close() } pub fn is_corked(&self) -> bool { false } - pub fn uncork(&mut self) {} + pub fn uncork(&self) {} pub fn is_connect_request(&self) -> bool { false } - pub fn prepare_for_sendfile(&mut self) {} - pub fn mark_needs_more(&mut self) {} - pub fn get_socket_data(&mut self) -> *mut c_void { + pub fn prepare_for_sendfile(&self) {} + pub fn mark_needs_more(&self) {} + pub fn get_socket_data(&self) -> *mut c_void { c::uws_h3_res_get_socket_data(self) } - pub fn get_remote_socket_info(&mut self) -> Option { + pub fn get_remote_socket_info(&self) -> Option { let mut port: i32 = 0; let mut is_ipv6: bool = false; let mut ip_ptr: *const u8 = ptr::null(); @@ -244,11 +244,11 @@ impl Response { let ip = unsafe { bun_core::ffi::slice(ip_ptr, len) }; Some(SocketAddress::new(ip, port, is_ipv6)) } - pub fn force_close(&mut self) { + pub fn force_close(&self) { c::uws_h3_res_force_close(self) } - pub fn on_writable(&mut self, _handler: H, ud: *mut UD) + pub fn on_writable(&self, _handler: H, ud: *mut UD) where H: Fn(&mut UD, u64, &mut Response) -> bool + Copy + 'static, { @@ -268,10 +268,10 @@ impl Response { } c::uws_h3_res_on_writable(self, Some(cb::), ud.cast()) } - pub fn clear_on_writable(&mut self) { + pub fn clear_on_writable(&self) { c::uws_h3_res_clear_on_writable(self) } - pub fn on_aborted(&mut self, _handler: H, ud: *mut UD) + pub fn on_aborted(&self, _handler: H, ud: *mut UD) where H: Fn(&mut UD, &mut Response) + Copy + 'static, { @@ -291,10 +291,10 @@ impl Response { } c::uws_h3_res_on_aborted(self, Some(cb::), ud.cast()) } - pub fn clear_aborted(&mut self) { + pub fn clear_aborted(&self) { c::uws_h3_res_on_aborted(self, None, ptr::null_mut()) } - pub fn on_timeout(&mut self, _handler: H, ud: *mut UD) + pub fn on_timeout(&self, _handler: H, ud: *mut UD) where H: Fn(&mut UD, &mut Response) + Copy + 'static, { @@ -314,10 +314,10 @@ impl Response { } c::uws_h3_res_on_timeout(self, Some(cb::), ud.cast()) } - pub fn clear_timeout(&mut self) { + pub fn clear_timeout(&self) { c::uws_h3_res_on_timeout(self, None, ptr::null_mut()) } - pub fn on_data(&mut self, _handler: H, ud: *mut UD) + pub fn on_data(&self, _handler: H, ud: *mut UD) where H: Fn(&mut UD, &mut Response, &[u8], bool) + Copy + 'static, { @@ -348,15 +348,15 @@ impl Response { } c::uws_h3_res_on_data(self, Some(cb::), ud.cast()) } - pub fn clear_on_data(&mut self) { + pub fn clear_on_data(&self) { c::uws_h3_res_on_data(self, None, ptr::null_mut()) } - pub fn corked(&mut self, handler: impl FnOnce()) { + pub fn corked(&self, handler: impl FnOnce()) { // H3 has no corking; call the handler immediately. let _ = self; handler(); } - pub fn run_corked_with_type(&mut self, handler: fn(*mut UD), ud: *mut UD) { + pub fn run_corked_with_type(&self, handler: fn(*mut UD), ud: *mut UD) { // cork is synchronous, so we stack-allocate the (handler, ud) pair and // recover it inside the trampoline — same shape as H1's // `Response::run_corked_with_type` so `AnyResponse` can dispatch uniformly. @@ -399,13 +399,13 @@ pub enum AddServerNameError { } bun_core::impl_tag_error!(AddServerNameError); -/// Stamps one `pub fn $name(&mut self, p, ud, h)` per HTTP verb, +/// Stamps one `pub fn $name(&self, p, ud, h)` per HTTP verb, /// each forwarding to [`App::route`] with the matching [`RouteKind`]. /// `connect`/`trace` are intentionally omitted — h3 exposes those only via /// [`App::method`]. macro_rules! h3_route_methods { ($($name:ident => $kind:ident),* $(,)?) => {$( - pub fn $name(&mut self, p: &[u8], ud: *mut UD, h: H) + pub fn $name(&self, p: &[u8], ud: *mut UD, h: H) where H: Fn(&mut UD, &mut Request, &mut Response) + Copy + 'static, { @@ -421,7 +421,7 @@ impl App { if p.is_null() { None } else { Some(p) } } pub fn add_server_name_with_options( - &mut self, + &self, hostname: &bun_core::ZStr, opts: &BunSocketContextOptions, ) -> Result<(), AddServerNameError> { @@ -438,14 +438,14 @@ impl App { // SAFETY: caller contract above unsafe { c::uws_h3_app_destroy(this) } } - pub fn close(&mut self) { + pub fn close(&self) { c::uws_h3_app_close(self) } - pub fn clear_routes(&mut self) { + pub fn clear_routes(&self) { c::uws_h3_app_clear_routes(self) } - fn route(which: RouteKind, this: &mut App, pattern: &[u8], ud: *mut UD, _handler: H) + fn route(which: RouteKind, this: &App, pattern: &[u8], ud: *mut UD, _handler: H) where H: Fn(&mut UD, &mut Request, &mut Response) + Copy + 'static, { @@ -499,7 +499,7 @@ impl App { any => Any, } - pub fn method(&mut self, m: bun_http_types::Method::Method, p: &[u8], ud: *mut UD, h: H) + pub fn method(&self, m: bun_http_types::Method::Method, p: &[u8], ud: *mut UD, h: H) where H: Fn(&mut UD, &mut Request, &mut Response) + Copy + 'static, { @@ -518,7 +518,7 @@ impl App { } } - pub fn listen_with_config(&mut self, ud: *mut UD, _handler: H, config: &ListenConfig) + pub fn listen_with_config(&self, ud: *mut UD, _handler: H, config: &ListenConfig) where H: Fn(&mut UD, Option<&mut ListenSocket>) + Copy + 'static, { @@ -595,161 +595,162 @@ mod c { idle_timeout_s: u32, ) -> *mut App; pub(super) fn uws_h3_app_destroy(app: *mut App); - pub(super) safe fn uws_h3_app_close(app: &mut App); - pub(super) safe fn uws_h3_app_clear_routes(app: &mut App); + pub(super) safe fn uws_h3_app_close(app: &App); + pub(super) safe fn uws_h3_app_clear_routes(app: &App); pub(super) fn uws_h3_app_add_server_name( - app: *mut App, + app: *const App, hostname: *const c_char, opts: BunSocketContextOptions, ) -> bool; - pub(super) safe fn uws_h3_res_write_continue(res: &mut Response); + pub(super) safe fn uws_h3_res_write_continue(res: &Response); pub(super) fn uws_h3_app_get( - app: *mut App, + app: *const App, p: *const u8, n: usize, h: Handler, ud: *mut c_void, ); pub(super) fn uws_h3_app_post( - app: *mut App, + app: *const App, p: *const u8, n: usize, h: Handler, ud: *mut c_void, ); pub(super) fn uws_h3_app_put( - app: *mut App, + app: *const App, p: *const u8, n: usize, h: Handler, ud: *mut c_void, ); pub(super) fn uws_h3_app_delete( - app: *mut App, + app: *const App, p: *const u8, n: usize, h: Handler, ud: *mut c_void, ); pub(super) fn uws_h3_app_patch( - app: *mut App, + app: *const App, p: *const u8, n: usize, h: Handler, ud: *mut c_void, ); pub(super) fn uws_h3_app_head( - app: *mut App, + app: *const App, p: *const u8, n: usize, h: Handler, ud: *mut c_void, ); pub(super) fn uws_h3_app_options( - app: *mut App, + app: *const App, p: *const u8, n: usize, h: Handler, ud: *mut c_void, ); pub(super) fn uws_h3_app_connect( - app: *mut App, + app: *const App, p: *const u8, n: usize, h: Handler, ud: *mut c_void, ); pub(super) fn uws_h3_app_trace( - app: *mut App, + app: *const App, p: *const u8, n: usize, h: Handler, ud: *mut c_void, ); pub(super) fn uws_h3_app_any( - app: *mut App, + app: *const App, p: *const u8, n: usize, h: Handler, ud: *mut c_void, ); pub(super) fn uws_h3_app_listen_with_config( - app: *mut App, + app: *const App, host: *const c_char, port: u16, options: i32, h: ListenHandler, ud: *mut c_void, ); - pub(super) safe fn uws_h3_listen_socket_port(ls: &mut ListenSocket) -> i32; + pub(super) safe fn uws_h3_listen_socket_port(ls: &ListenSocket) -> i32; pub(super) fn uws_h3_listen_socket_local_address( - ls: *mut ListenSocket, + ls: *const ListenSocket, buf: *mut u8, len: c_int, ) -> c_int; - pub(super) safe fn uws_h3_listen_socket_close(ls: &mut ListenSocket); + pub(super) safe fn uws_h3_listen_socket_close(ls: &ListenSocket); - pub(super) safe fn uws_h3_res_state(res: &mut Response) -> State; - pub(super) fn uws_h3_res_end(res: *mut Response, p: *const u8, n: usize, close: bool); - pub(super) safe fn uws_h3_res_end_stream(res: &mut Response, close: bool); - pub(super) safe fn uws_h3_res_force_close(res: &mut Response); + pub(super) safe fn uws_h3_res_state(res: &Response) -> State; + pub(super) fn uws_h3_res_end(res: *const Response, p: *const u8, n: usize, close: bool); + pub(super) safe fn uws_h3_res_end_stream(res: &Response, close: bool); + pub(super) safe fn uws_h3_res_force_close(res: &Response); pub(super) fn uws_h3_res_try_end( - res: *mut Response, + res: *const Response, p: *const u8, n: usize, total: usize, close: bool, ) -> bool; - pub(super) safe fn uws_h3_res_end_without_body(res: &mut Response, close: bool); - pub(super) safe fn uws_h3_res_pause(res: &mut Response); - pub(super) safe fn uws_h3_res_resume(res: &mut Response); - pub(super) fn uws_h3_res_write_status(res: *mut Response, p: *const u8, n: usize); + pub(super) safe fn uws_h3_res_end_without_body(res: &Response, close: bool); + pub(super) safe fn uws_h3_res_pause(res: &Response); + pub(super) safe fn uws_h3_res_resume(res: &Response); + pub(super) fn uws_h3_res_write_status(res: *const Response, p: *const u8, n: usize); pub(super) fn uws_h3_res_write_header( - res: *mut Response, + res: *const Response, kp: *const u8, kn: usize, vp: *const u8, vn: usize, ); pub(super) fn uws_h3_res_write_header_int( - res: *mut Response, + res: *const Response, kp: *const u8, kn: usize, v: u64, ); - pub(super) safe fn uws_h3_res_mark_wrote_content_length_header(res: &mut Response); - pub(super) safe fn uws_h3_res_write_mark(res: &mut Response); - pub(super) safe fn uws_h3_res_flush_headers(res: &mut Response, immediate: bool); - pub(super) fn uws_h3_res_write(res: *mut Response, p: *const u8, len: *mut usize) -> bool; - pub(super) safe fn uws_h3_res_get_write_offset(res: &mut Response) -> u64; - pub(super) safe fn uws_h3_res_override_write_offset(res: &mut Response, off: u64); - pub(super) safe fn uws_h3_res_has_responded(res: &mut Response) -> bool; - pub(super) safe fn uws_h3_res_get_buffered_amount(res: &mut Response) -> u64; - pub(super) safe fn uws_h3_res_reset_timeout(res: &mut Response); - pub(super) safe fn uws_h3_res_timeout(res: &mut Response, seconds: u8); - pub(super) safe fn uws_h3_res_end_sendfile(res: &mut Response, off: u64, close: bool); - pub(super) safe fn uws_h3_res_get_socket_data(res: &mut Response) -> *mut c_void; - // safe: `&mut Response` is ABI-identical to a non-null `*mut`; + pub(super) safe fn uws_h3_res_mark_wrote_content_length_header(res: &Response); + pub(super) safe fn uws_h3_res_write_mark(res: &Response); + pub(super) safe fn uws_h3_res_flush_headers(res: &Response, immediate: bool); + pub(super) fn uws_h3_res_write(res: *const Response, p: *const u8, len: *mut usize) + -> bool; + pub(super) safe fn uws_h3_res_get_write_offset(res: &Response) -> u64; + pub(super) safe fn uws_h3_res_override_write_offset(res: &Response, off: u64); + pub(super) safe fn uws_h3_res_has_responded(res: &Response) -> bool; + pub(super) safe fn uws_h3_res_get_buffered_amount(res: &Response) -> u64; + pub(super) safe fn uws_h3_res_reset_timeout(res: &Response); + pub(super) safe fn uws_h3_res_timeout(res: &Response, seconds: u8); + pub(super) safe fn uws_h3_res_end_sendfile(res: &Response, off: u64, close: bool); + pub(super) safe fn uws_h3_res_get_socket_data(res: &Response) -> *mut c_void; + // safe: `&Response` is ABI-identical to a non-null pointer; // `cb`/`ud` are stored opaquely (never dereferenced by the C++ shim // itself) — no preconditions on this call. Mirrors `uws_res_on_*`. pub(super) safe fn uws_h3_res_on_writable( - res: &mut Response, + res: &Response, cb: Option bool>, ud: *mut c_void, ); - pub(super) safe fn uws_h3_res_clear_on_writable(res: &mut Response); + pub(super) safe fn uws_h3_res_clear_on_writable(res: &Response); pub(super) safe fn uws_h3_res_on_aborted( - res: &mut Response, + res: &Response, cb: Option, ud: *mut c_void, ); pub(super) safe fn uws_h3_res_on_timeout( - res: &mut Response, + res: &Response, cb: Option, ud: *mut c_void, ); pub(super) safe fn uws_h3_res_on_data( - res: &mut Response, + res: &Response, cb: Option, ud: *mut c_void, ); @@ -757,50 +758,46 @@ mod c { // without being dereferenced by the C++ shim itself, so the call has // no preconditions beyond the live opaque handle. pub(super) safe fn uws_h3_res_cork( - res: &mut Response, + res: &Response, ud: *mut c_void, cb: unsafe extern "C" fn(*mut c_void), ); // Out-params are `&mut` (non-null, valid for write); the C shim only // stores into them and returns a length — no read-through precondition. pub(super) safe fn uws_h3_res_get_remote_address_info( - res: &mut Response, + res: &Response, ip: &mut *const u8, port: &mut i32, is_ipv6: &mut bool, ) -> usize; - pub(super) safe fn uws_h3_req_get_yield(req: &mut Request) -> bool; - pub(super) safe fn uws_h3_req_set_yield(req: &mut Request, y: bool); + pub(super) safe fn uws_h3_req_get_yield(req: &Request) -> bool; + pub(super) safe fn uws_h3_req_set_yield(req: &Request, y: bool); // Out-param `out` is `&mut *const u8` (non-null, valid for write); the C // shim only stores a pointer into request-owned storage and returns its // length — no read-through precondition, so `safe fn`. - pub(super) safe fn uws_h3_req_get_url(req: &mut Request, out: &mut *const u8) -> usize; - pub(super) safe fn uws_h3_req_get_method(req: &mut Request, out: &mut *const u8) -> usize; + pub(super) safe fn uws_h3_req_get_url(req: &Request, out: &mut *const u8) -> usize; + pub(super) safe fn uws_h3_req_get_method(req: &Request, out: &mut *const u8) -> usize; pub(super) fn uws_h3_req_get_header( - req: *mut Request, + req: *const Request, name: *const u8, len: usize, out: *mut *const u8, ) -> usize; pub(super) fn uws_h3_req_get_query( - req: *mut Request, + req: *const Request, name: *const u8, len: usize, out: *mut *const u8, ) -> usize; pub(super) safe fn uws_h3_req_get_parameter( - req: &mut Request, + req: &Request, idx: u16, out: &mut *const u8, ) -> usize; // safe: synchronous header iteration — `ud` is forwarded opaquely to // `cb` without being dereferenced by the C++ shim itself; `cb` is a // by-value fn pointer. No preconditions beyond the live opaque handle. - pub(super) safe fn uws_h3_req_for_each_header( - req: &mut Request, - cb: HeaderCb, - ud: *mut c_void, - ); + pub(super) safe fn uws_h3_req_for_each_header(req: &Request, cb: HeaderCb, ud: *mut c_void); } } diff --git a/src/uws_sys/lib.rs b/src/uws_sys/lib.rs index bd16222a618e..b2fab73351b9 100644 --- a/src/uws_sys/lib.rs +++ b/src/uws_sys/lib.rs @@ -31,8 +31,9 @@ pub const LIBUS_SOCKET_IPV6_ONLY: core::ffi::c_int = 8; pub const LIBUS_LISTEN_REUSE_ADDR: core::ffi::c_int = 16; pub const LIBUS_LISTEN_DISALLOW_REUSE_PORT_FAILURE: core::ffi::c_int = 32; -/// BoringSSL `SSL_CTX` (alias so callers don't need a direct boringssl dep). -pub type SslCtx = bun_boringssl_sys::SSL_CTX; +/// The BoringSSL `SSL_CTX` C object (alias so callers don't need a direct +/// boringssl dep). The owning handle is `bun_boringssl_sys::SSL_CTX`. +pub type SslCtx = bun_boringssl_sys::sys::SSL_CTX; /// `struct us_bun_verify_error_t` — TLS handshake verification result. /// diff --git a/src/uws_sys/quic/Context.rs b/src/uws_sys/quic/Context.rs index 880c6091783a..d17c64b49868 100644 --- a/src/uws_sys/quic/Context.rs +++ b/src/uws_sys/quic/Context.rs @@ -17,14 +17,13 @@ unsafe extern "C" { stream_ext: c_uint, ) -> *mut Context; - // `Context` is an `opaque_ffi!` ZST (`UnsafeCell<[u8; 0]>`), so - // `&mut Context` is ABI-identical to a non-null `*mut Context` with no - // `noalias`/`readonly` attribute. Shims taking only the handle + value - // types (incl. fn-pointer callbacks) are `safe fn`. - safe fn us_quic_socket_context_loop(ctx: &mut Context) -> *mut Loop; + // `&Context` is ABI-identical to a non-null `*mut Context` and carries no + // `noalias`/`readonly` — lsquic mutates freely through it. Shims taking only + // the handle + value types (incl. fn-pointer callbacks) are `safe fn`. + safe fn us_quic_socket_context_loop(ctx: &Context) -> *mut Loop; fn us_quic_socket_context_connect( - ctx: *mut Context, + ctx: &Context, host: *const c_char, port: c_int, sni: *const c_char, @@ -35,35 +34,29 @@ unsafe extern "C" { ) -> c_int; safe fn us_quic_socket_context_on_hsk_done( - ctx: &mut Context, + ctx: &Context, cb: unsafe extern "C" fn(*mut Socket, c_int), ); - safe fn us_quic_socket_context_on_goaway( - ctx: &mut Context, - cb: unsafe extern "C" fn(*mut Socket), - ); - safe fn us_quic_socket_context_on_close( - ctx: &mut Context, - cb: unsafe extern "C" fn(*mut Socket), - ); + safe fn us_quic_socket_context_on_goaway(ctx: &Context, cb: unsafe extern "C" fn(*mut Socket)); + safe fn us_quic_socket_context_on_close(ctx: &Context, cb: unsafe extern "C" fn(*mut Socket)); safe fn us_quic_socket_context_on_stream_open( - ctx: &mut Context, + ctx: &Context, cb: unsafe extern "C" fn(*mut Stream, c_int), ); safe fn us_quic_socket_context_on_stream_headers( - ctx: &mut Context, + ctx: &Context, cb: unsafe extern "C" fn(*mut Stream), ); safe fn us_quic_socket_context_on_stream_data( - ctx: &mut Context, + ctx: &Context, cb: unsafe extern "C" fn(*mut Stream, *const u8, c_uint, c_int), ); safe fn us_quic_socket_context_on_stream_writable( - ctx: &mut Context, + ctx: &Context, cb: unsafe extern "C" fn(*mut Stream), ); safe fn us_quic_socket_context_on_stream_close( - ctx: &mut Context, + ctx: &Context, cb: unsafe extern "C" fn(*mut Stream), ); } @@ -96,7 +89,7 @@ impl Context { } #[inline] - pub fn r#loop(&mut self) -> *mut Loop { + pub fn r#loop(&self) -> *mut Loop { // Returns a raw pointer because the Loop is shared across every // context/socket/timer on the thread — // materializing `&mut Loop` here would assert uniqueness we cannot @@ -105,7 +98,7 @@ impl Context { } pub fn connect( - &mut self, + &self, host: &CStr, port: u16, sni: &CStr, @@ -135,38 +128,35 @@ impl Context { } #[inline] - pub fn on_hsk_done(&mut self, cb: unsafe extern "C" fn(*mut Socket, c_int)) { + pub fn on_hsk_done(&self, cb: unsafe extern "C" fn(*mut Socket, c_int)) { us_quic_socket_context_on_hsk_done(self, cb) } #[inline] - pub fn on_goaway(&mut self, cb: unsafe extern "C" fn(*mut Socket)) { + pub fn on_goaway(&self, cb: unsafe extern "C" fn(*mut Socket)) { us_quic_socket_context_on_goaway(self, cb) } #[inline] - pub fn on_close(&mut self, cb: unsafe extern "C" fn(*mut Socket)) { + pub fn on_close(&self, cb: unsafe extern "C" fn(*mut Socket)) { us_quic_socket_context_on_close(self, cb) } #[inline] - pub fn on_stream_open(&mut self, cb: unsafe extern "C" fn(*mut Stream, c_int)) { + pub fn on_stream_open(&self, cb: unsafe extern "C" fn(*mut Stream, c_int)) { us_quic_socket_context_on_stream_open(self, cb) } #[inline] - pub fn on_stream_headers(&mut self, cb: unsafe extern "C" fn(*mut Stream)) { + pub fn on_stream_headers(&self, cb: unsafe extern "C" fn(*mut Stream)) { us_quic_socket_context_on_stream_headers(self, cb) } #[inline] - pub fn on_stream_data( - &mut self, - cb: unsafe extern "C" fn(*mut Stream, *const u8, c_uint, c_int), - ) { + pub fn on_stream_data(&self, cb: unsafe extern "C" fn(*mut Stream, *const u8, c_uint, c_int)) { us_quic_socket_context_on_stream_data(self, cb) } #[inline] - pub fn on_stream_writable(&mut self, cb: unsafe extern "C" fn(*mut Stream)) { + pub fn on_stream_writable(&self, cb: unsafe extern "C" fn(*mut Stream)) { us_quic_socket_context_on_stream_writable(self, cb) } #[inline] - pub fn on_stream_close(&mut self, cb: unsafe extern "C" fn(*mut Stream)) { + pub fn on_stream_close(&self, cb: unsafe extern "C" fn(*mut Stream)) { us_quic_socket_context_on_stream_close(self, cb) } } diff --git a/src/uws_sys/quic/PendingConnect.rs b/src/uws_sys/quic/PendingConnect.rs index aa3f6f096d67..6e2866e8e2a5 100644 --- a/src/uws_sys/quic/PendingConnect.rs +++ b/src/uws_sys/quic/PendingConnect.rs @@ -4,6 +4,7 @@ //! Consumed by exactly one of `resolved()` or `cancel()`. use core::ffi::c_void; +use core::ptr::NonNull; use crate::quic::Socket; @@ -13,26 +14,29 @@ bun_opaque::opaque_ffi! { } // `PendingConnect` is an `opaque_ffi!` ZST (`UnsafeCell<[u8; 0]>`), so -// `&mut PendingConnect` is ABI-identical to a non-null `*mut PendingConnect` +// `&PendingConnect` is ABI-identical to a non-null `*mut PendingConnect` // with no `noalias`/`readonly` attribute — handle-only shims are `safe fn`. unsafe extern "C" { - safe fn us_quic_pending_connect_addrinfo(pc: &mut PendingConnect) -> *mut c_void; - safe fn us_quic_pending_connect_resolved(pc: &mut PendingConnect) -> *mut Socket; - safe fn us_quic_pending_connect_cancel(pc: &mut PendingConnect); + safe fn us_quic_pending_connect_addrinfo(pc: &PendingConnect) -> *mut c_void; + safe fn us_quic_pending_connect_resolved(pc: &PendingConnect) -> *mut Socket; + safe fn us_quic_pending_connect_cancel(pc: &PendingConnect); } impl PendingConnect { - pub fn addrinfo(&mut self) -> *mut c_void { + pub fn addrinfo(&self) -> *mut c_void { us_quic_pending_connect_addrinfo(self) } - pub fn resolved(&mut self) -> Option<&mut Socket> { - // SAFETY: C returns null or a valid `us_quic_socket_t*`; `Socket` is an - // opaque ZST handle so `&mut` carries no aliasing assumptions. - unsafe { us_quic_pending_connect_resolved(self).as_mut() } + /// The connected socket, or `None` if the name lookup failed. + /// + /// Returns `NonNull`, not `&mut Socket`: the socket is C-owned, and minting a + /// `&mut` from `&self` would let two live `&mut Socket` exist (and trips + /// `clippy::mut_from_ref`). Callers reborrow via `Socket::opaque_mut`. + pub fn resolved(&self) -> Option> { + NonNull::new(us_quic_pending_connect_resolved(self)) } - pub fn cancel(&mut self) { + pub fn cancel(&self) { us_quic_pending_connect_cancel(self) } } diff --git a/src/uws_sys/quic/Stream.rs b/src/uws_sys/quic/Stream.rs index 503eecfafb5c..9d8512f0a0df 100644 --- a/src/uws_sys/quic/Stream.rs +++ b/src/uws_sys/quic/Stream.rs @@ -12,23 +12,22 @@ bun_opaque::opaque_ffi! { pub struct Stream; } -// `Stream` is an `opaque_ffi!` ZST (`UnsafeCell<[u8; 0]>`), so `&mut Stream` is -// ABI-identical to a non-null `*mut Stream` with no `noalias`/`readonly` -// attribute. Shims taking only the handle + value types are `safe fn`; the -// (ptr,len) writers keep raw signatures. +// `Stream` is an `opaque_ffi!` ZST (`UnsafeCell<[u8; 0]>`): `&Stream` is ABI-identical +// to a non-null `us_quic_stream_t*` and carries no `noalias`/`readonly`, so lsquic +// mutates through it. Handle + value-type shims are `safe fn`; (ptr,len) writers are not. unsafe extern "C" { - safe fn us_quic_stream_socket(s: &mut Stream) -> *mut Socket; - safe fn us_quic_stream_shutdown(s: &mut Stream); - safe fn us_quic_stream_close(s: &mut Stream); - safe fn us_quic_stream_reset(s: &mut Stream); - safe fn us_quic_stream_header_count(s: &mut Stream) -> c_uint; - safe fn us_quic_stream_header(s: &mut Stream, i: c_uint) -> *const Header; - safe fn us_quic_stream_ext(s: &mut Stream) -> *mut c_void; - fn us_quic_stream_write(s: *mut Stream, data: *const u8, len: c_uint) -> c_int; - safe fn us_quic_stream_want_write(s: &mut Stream, want: c_int); - safe fn us_quic_stream_want_read(s: &mut Stream, want: c_int); + safe fn us_quic_stream_socket(s: &Stream) -> *mut Socket; + safe fn us_quic_stream_shutdown(s: &Stream); + safe fn us_quic_stream_close(s: &Stream); + safe fn us_quic_stream_reset(s: &Stream); + safe fn us_quic_stream_header_count(s: &Stream) -> c_uint; + safe fn us_quic_stream_header(s: &Stream, i: c_uint) -> *const Header; + safe fn us_quic_stream_ext(s: &Stream) -> *mut c_void; + fn us_quic_stream_write(s: &Stream, data: *const u8, len: c_uint) -> c_int; + safe fn us_quic_stream_want_write(s: &Stream, want: c_int); + safe fn us_quic_stream_want_read(s: &Stream, want: c_int); fn us_quic_stream_send_headers( - s: *mut Stream, + s: &Stream, h: *const Header, n: c_uint, end_stream: c_int, @@ -36,44 +35,42 @@ unsafe extern "C" { } impl Stream { - pub fn socket(&mut self) -> Option> { - // Returned as a raw pointer (not &mut) because the Socket is the *parent - // connection shared by every stream on it* — two live &mut Stream on the - // same conn calling .socket() (or a conn-level callback already holding - // &mut Socket) would otherwise yield aliasing &mut Socket, which is UB. - // Callers reborrow locally under their own SAFETY proof. + pub fn socket(&self) -> Option> { + // Raw pointer (not &mut) because the Socket is the *parent connection shared by + // every stream on it* — a conn-level callback may already hold &mut Socket, so + // minting one here would alias. Callers reborrow under their own SAFETY proof. NonNull::new(us_quic_stream_socket(self)) } - pub fn shutdown(&mut self) { + pub fn shutdown(&self) { us_quic_stream_shutdown(self) } - pub fn close(&mut self) { + pub fn close(&self) { us_quic_stream_close(self) } - pub fn reset(&mut self) { + pub fn reset(&self) { us_quic_stream_reset(self) } - pub fn header_count(&mut self) -> c_uint { + pub fn header_count(&self) -> c_uint { us_quic_stream_header_count(self) } - pub fn header(&mut self, i: c_uint) -> Option<&Header> { + pub fn header(&self, i: c_uint) -> Option<&Header> { // SAFETY: self is a valid us_quic_stream_t; returned header borrowed from stream's header block. unsafe { us_quic_stream_header(self, i).as_ref() } } - pub fn ext(&mut self) -> &Cell>> { + pub fn ext(&self) -> &Cell>> { // SAFETY: self is a valid us_quic_stream_t; ext slot is pointer-sized & pointer-aligned, // and Option> has nullable-pointer layout. `Cell` is repr(transparent), so no // &mut into the slot is ever live across a callback that re-enters lsquic. unsafe { &*us_quic_stream_ext(self).cast::>>>() } } - pub fn write(&mut self, data: &[u8]) -> c_int { + pub fn write(&self, data: &[u8]) -> c_int { // SAFETY: self is a valid us_quic_stream_t; data.ptr valid for data.len() bytes. unsafe { us_quic_stream_write( @@ -84,15 +81,15 @@ impl Stream { } } - pub fn want_write(&mut self, want: bool) { + pub fn want_write(&self, want: bool) { us_quic_stream_want_write(self, want as c_int) } - pub fn want_read(&mut self, want: bool) { + pub fn want_read(&self, want: bool) { us_quic_stream_want_read(self, want as c_int) } - pub fn send_headers(&mut self, headers: &[Header], end_stream: bool) -> c_int { + pub fn send_headers(&self, headers: &[Header], end_stream: bool) -> c_int { // SAFETY: self is a valid us_quic_stream_t; headers.ptr valid for headers.len() entries. unsafe { us_quic_stream_send_headers( diff --git a/src/uws_sys/socket.rs b/src/uws_sys/socket.rs index 6f377c8b0228..ad2e18160c92 100644 --- a/src/uws_sys/socket.rs +++ b/src/uws_sys/socket.rs @@ -122,7 +122,8 @@ impl InternalSocket { fn sock<'a>(p: *mut us_socket_t) -> &'a mut us_socket_t { bun_opaque::opaque_deref_mut(p) } -/// Reborrow the `InternalSocket::Connecting` payload. +/// Reborrow the `InternalSocket::Connecting` payload. `&mut`, mirroring `sock`: +/// `ext` hands out a `&mut T` into the socket's real trailing ext storage. #[inline(always)] fn conn<'a>(p: *mut ConnectingSocket) -> &'a mut ConnectingSocket { bun_opaque::opaque_deref_mut(p) @@ -558,8 +559,7 @@ impl NewSocketHandler { _ => { // The socket is gone; release the reference the caller handed us. if !ctx.is_null() { - // SAFETY: the caller passed an owned SSL_CTX reference. - unsafe { bun_boringssl_sys::SSL_CTX_free(ctx) }; + bun_boringssl_sys::SSL_CTX_free(crate::SslCtx::opaque_ref(ctx)); } } } diff --git a/src/uws_sys/udp.rs b/src/uws_sys/udp.rs index 8a69b996fe40..1db99fdc6234 100644 --- a/src/uws_sys/udp.rs +++ b/src/uws_sys/udp.rs @@ -48,7 +48,7 @@ impl Socket { } pub fn send( - &mut self, + &self, payloads: &[*const u8], lengths: &[usize], addresses: &[*const c_void], @@ -66,60 +66,60 @@ impl Socket { } } - pub fn user(&mut self) -> *mut c_void { + pub fn user(&self) -> *mut c_void { us_udp_socket_user(self) } /// Get the bound port in host byte order - pub fn bound_port(&mut self) -> c_int { + pub fn bound_port(&self) -> c_int { us_udp_socket_bound_port(self) } - pub fn bound_ip(&mut self, buf: *mut u8, length: &mut i32) { + pub fn bound_ip(&self, buf: *mut u8, length: &mut i32) { // SAFETY: buf must point to at least *length bytes; thin FFI passthrough. unsafe { us_udp_socket_bound_ip(self, buf, length) } } - pub fn remote_ip(&mut self, buf: *mut u8, length: &mut i32) { + pub fn remote_ip(&self, buf: *mut u8, length: &mut i32) { // SAFETY: buf must point to at least *length bytes; thin FFI passthrough. unsafe { us_udp_socket_remote_ip(self, buf, length) } } - pub fn close(&mut self) { + pub fn close(&self) { us_udp_socket_close(self) } - pub fn connect(&mut self, hostname: *const c_char, port: c_uint) -> c_int { + pub fn connect(&self, hostname: *const c_char, port: c_uint) -> c_int { // SAFETY: thin FFI passthrough; hostname must be NUL-terminated per uSockets. unsafe { us_udp_socket_connect(self, hostname, port) } } - pub fn disconnect(&mut self) -> c_int { + pub fn disconnect(&self) -> c_int { us_udp_socket_disconnect(self) } - pub fn set_broadcast(&mut self, enabled: bool) -> c_int { + pub fn set_broadcast(&self, enabled: bool) -> c_int { us_udp_socket_set_broadcast(self, enabled as c_int) } - pub fn set_unicast_ttl(&mut self, ttl: i32) -> c_int { + pub fn set_unicast_ttl(&self, ttl: i32) -> c_int { us_udp_socket_set_ttl_unicast(self, ttl as c_int) } - pub fn set_multicast_ttl(&mut self, ttl: i32) -> c_int { + pub fn set_multicast_ttl(&self, ttl: i32) -> c_int { us_udp_socket_set_ttl_multicast(self, ttl as c_int) } - pub fn set_multicast_loopback(&mut self, enabled: bool) -> c_int { + pub fn set_multicast_loopback(&self, enabled: bool) -> c_int { us_udp_socket_set_multicast_loopback(self, enabled as c_int) } - pub fn set_multicast_interface(&mut self, iface: &sockaddr_storage) -> c_int { + pub fn set_multicast_interface(&self, iface: &sockaddr_storage) -> c_int { us_udp_socket_set_multicast_interface(self, iface) } pub fn set_membership( - &mut self, + &self, address: &sockaddr_storage, iface: Option<&sockaddr_storage>, drop: bool, @@ -128,7 +128,7 @@ impl Socket { } pub fn set_source_specific_membership( - &mut self, + &self, source: &sockaddr_storage, group: &sockaddr_storage, iface: Option<&sockaddr_storage>, @@ -138,6 +138,9 @@ impl Socket { } } +// `Socket` is an `opaque_ffi!` ZST: `&Socket` is ABI-identical to a non-null +// `us_udp_socket_t*` and carries no `noalias`/`readonly` — C owns the socket and +// mutates it through the same pointer, so no shim takes `&mut Socket`. unsafe extern "C" { fn us_create_udp_socket( loop_: *mut Loop, @@ -151,26 +154,26 @@ unsafe extern "C" { err: *mut c_int, user_data: *mut c_void, ) -> *mut Socket; - fn us_udp_socket_connect(socket: *mut Socket, hostname: *const c_char, port: c_uint) -> c_int; - safe fn us_udp_socket_disconnect(socket: &mut Socket) -> c_int; + fn us_udp_socket_connect(socket: &Socket, hostname: *const c_char, port: c_uint) -> c_int; + safe fn us_udp_socket_disconnect(socket: &Socket) -> c_int; fn us_udp_socket_send( - socket: *mut Socket, + socket: &Socket, payloads: *const *const u8, lengths: *const usize, addresses: *const *const c_void, num: c_int, ) -> c_int; - safe fn us_udp_socket_user(socket: &mut Socket) -> *mut c_void; - safe fn us_udp_socket_bound_port(socket: &mut Socket) -> c_int; - fn us_udp_socket_bound_ip(socket: *mut Socket, buf: *mut u8, length: *mut i32); - fn us_udp_socket_remote_ip(socket: *mut Socket, buf: *mut u8, length: *mut i32); - safe fn us_udp_socket_close(socket: &mut Socket); - safe fn us_udp_socket_set_broadcast(socket: &mut Socket, enabled: c_int) -> c_int; - safe fn us_udp_socket_set_ttl_unicast(socket: &mut Socket, ttl: c_int) -> c_int; - safe fn us_udp_socket_set_ttl_multicast(socket: &mut Socket, ttl: c_int) -> c_int; - safe fn us_udp_socket_set_multicast_loopback(socket: &mut Socket, enabled: c_int) -> c_int; + safe fn us_udp_socket_user(socket: &Socket) -> *mut c_void; + safe fn us_udp_socket_bound_port(socket: &Socket) -> c_int; + fn us_udp_socket_bound_ip(socket: &Socket, buf: *mut u8, length: *mut i32); + fn us_udp_socket_remote_ip(socket: &Socket, buf: *mut u8, length: *mut i32); + safe fn us_udp_socket_close(socket: &Socket); + safe fn us_udp_socket_set_broadcast(socket: &Socket, enabled: c_int) -> c_int; + safe fn us_udp_socket_set_ttl_unicast(socket: &Socket, ttl: c_int) -> c_int; + safe fn us_udp_socket_set_ttl_multicast(socket: &Socket, ttl: c_int) -> c_int; + safe fn us_udp_socket_set_multicast_loopback(socket: &Socket, enabled: c_int) -> c_int; safe fn us_udp_socket_set_multicast_interface( - socket: &mut Socket, + socket: &Socket, iface: &sockaddr_storage, ) -> c_int; // `Option<&sockaddr_storage>` is FFI-safe (null-pointer niche → `*const`); @@ -178,13 +181,13 @@ unsafe extern "C" { // arg either a reference or a niche-optimized `Option<&T>`, the validity // proof is in the type signature — no remaining preconditions, so `safe fn`. safe fn us_udp_socket_set_membership( - socket: &mut Socket, + socket: &Socket, address: &sockaddr_storage, iface: Option<&sockaddr_storage>, drop: c_int, ) -> c_int; safe fn us_udp_socket_set_source_specific_membership( - socket: &mut Socket, + socket: &Socket, source: &sockaddr_storage, group: &sockaddr_storage, iface: Option<&sockaddr_storage>, @@ -221,7 +224,7 @@ impl PacketBuffer { } } - pub fn get_truncated(&mut self, index: c_int) -> bool { + pub fn get_truncated(&self, index: c_int) -> bool { us_udp_packet_buffer_truncated(self, index) != 0 } } @@ -233,5 +236,5 @@ unsafe extern "C" { ) -> *mut sockaddr_storage; safe fn us_udp_packet_buffer_payload(buf: &mut PacketBuffer, index: c_int) -> *mut u8; safe fn us_udp_packet_buffer_payload_length(buf: &mut PacketBuffer, index: c_int) -> c_int; - safe fn us_udp_packet_buffer_truncated(buf: &mut PacketBuffer, index: c_int) -> c_int; + safe fn us_udp_packet_buffer_truncated(buf: &PacketBuffer, index: c_int) -> c_int; } diff --git a/src/uws_sys/us_socket_t.rs b/src/uws_sys/us_socket_t.rs index e4e148e4e6e9..8999e6f0e2a8 100644 --- a/src/uws_sys/us_socket_t.rs +++ b/src/uws_sys/us_socket_t.rs @@ -41,14 +41,14 @@ pub struct UsIoVec { } impl us_socket_t { - pub fn open(&mut self, is_client: bool, ip_addr: Option<&[u8]>) { + pub fn open(&self, is_client: bool, ip_addr: Option<&[u8]>) { bun_core::scoped_log!(uws, "us_socket_open({:p}, is_client: {})", self, is_client); if let Some(ip) = ip_addr { debug_assert!(ip.len() < MAX_I32); unsafe { // SAFETY: self is a live us_socket_t; ip.ptr valid for ip.len bytes let _ = c::us_socket_open( - self, + self.as_mut_ptr(), is_client as i32, ip.as_ptr(), i32::try_from(ip.len().min(MAX_I32)).expect("int cast"), @@ -57,22 +57,22 @@ impl us_socket_t { } else { unsafe { // SAFETY: self is a live us_socket_t - let _ = c::us_socket_open(self, is_client as i32, ptr::null(), 0); + let _ = c::us_socket_open(self.as_mut_ptr(), is_client as i32, ptr::null(), 0); } } } - pub fn pause(&mut self) { + pub fn pause(&self) { bun_core::scoped_log!(uws, "us_socket_pause({:p})", self); c::us_socket_pause(self); } - pub fn resume(&mut self) { + pub fn resume(&self) { bun_core::scoped_log!(uws, "us_socket_resume({:p})", self); c::us_socket_resume(self); } - pub fn close(&mut self, code: CloseCode) { + pub fn close(&self, code: CloseCode) { bun_core::scoped_log!( uws, "us_socket_close({:p}, {})", @@ -81,16 +81,16 @@ impl us_socket_t { ); unsafe { // SAFETY: self is a live us_socket_t - let _ = c::us_socket_close(self, code, ptr::null_mut()); + let _ = c::us_socket_close(self.as_mut_ptr(), code, ptr::null_mut()); } } - pub fn shutdown(&mut self) { + pub fn shutdown(&self) { bun_core::scoped_log!(uws, "us_socket_shutdown({:p})", self); c::us_socket_shutdown(self); } - pub fn shutdown_read(&mut self) { + pub fn shutdown_read(&self) { c::us_socket_shutdown_read(self); } @@ -163,42 +163,43 @@ impl us_socket_t { Ok(&buf[..usize::try_from(length).expect("int cast")]) } - pub fn set_timeout(&mut self, seconds: u32) { + pub fn set_timeout(&self, seconds: u32) { c::us_socket_timeout(self, seconds); } - pub fn set_long_timeout(&mut self, minutes: u32) { + pub fn set_long_timeout(&self, minutes: u32) { c::us_socket_long_timeout(self, minutes); } - pub fn set_nodelay(&mut self, enabled: bool) { + pub fn set_nodelay(&self, enabled: bool) { c::us_socket_nodelay(self, enabled as c_int); } - pub fn set_keepalive(&mut self, enabled: bool, delay: u32) -> i32 { + pub fn set_keepalive(&self, enabled: bool, delay: u32) -> i32 { c::us_socket_keepalive(self, enabled as c_int, delay) } /// Set the IP type-of-service / traffic class. Returns 0 on success or a /// negative platform errno. - pub fn set_tos(&mut self, tos: i32) -> i32 { + pub fn set_tos(&self, tos: i32) -> i32 { c::us_socket_set_tos(self, tos) } /// Get the IP type-of-service / traffic class (>= 0) or a negative errno. - pub fn get_tos(&mut self) -> i32 { + pub fn get_tos(&self) -> i32 { c::us_socket_get_tos(self) } /// Resume a handshake suspended by an asynchronous SNICallback. `ctx` /// carries an owned SSL_CTX reference that the call consumes (may be /// null = fall through to the default context); `error` aborts instead. - pub fn sni_resolve(&mut self, ctx: *mut SslCtx, error: bool) { + pub fn sni_resolve(&self, ctx: *mut SslCtx, error: bool) { c::us_socket_sni_resolve(self, ctx, error as c_int); } /// `SSL*` if TLS, else null. Use `get_fd()` for the descriptor. - pub fn ssl(&mut self) -> Option<&mut bun_boringssl_sys::SSL> { + #[allow(clippy::mut_from_ref)] + pub fn ssl(&self) -> Option<&mut bun_boringssl_sys::SSL> { if !self.is_tls() { return None; } @@ -213,7 +214,7 @@ impl us_socket_t { /// Node-compat `_handle` shape: `SSL*` for TLS sockets, fd-as-pointer for /// plain TCP. Consumers that want one or the other should call `ssl()` / /// `get_fd()` directly; this is the round-trip-to-JS form. - pub fn get_native_handle(&mut self) -> Option<*mut c_void> { + pub fn get_native_handle(&self) -> Option<*mut c_void> { let p = c::us_socket_get_native_handle(self); if p.is_null() { None } else { Some(p) } } @@ -228,7 +229,7 @@ impl us_socket_t { /// Type-erased ext storage — `LIBUS_EXT_ALIGNMENT`-aligned bytes /// immediately after the C struct. Prefer `ext()`. - pub fn ext_ptr(&mut self) -> *mut u8 { + pub fn ext_ptr(&self) -> *mut u8 { c::us_socket_ext(self).cast::() } @@ -250,7 +251,7 @@ impl us_socket_t { /// Re-stamp the dispatch kind in place. Used after `Listener.onCreate` /// stashes the `NewSocket*` in ext so subsequent events skip the listener /// arm and route straight to `BunSocket`. - pub fn set_kind(&mut self, k: SocketKind) { + pub fn set_kind(&self, k: SocketKind) { c::us_socket_set_kind(self, k as u8); } @@ -258,14 +259,22 @@ impl us_socket_t { /// Returns the (possibly relocated) socket; `self` is invalid after. // TODO: take `self` by value — it is consumed/invalidated; the returned ptr may be a different allocation pub fn adopt( - &mut self, + &self, g: &mut SocketGroup, k: SocketKind, old_ext: i32, new_ext: i32, ) -> Option> { // SAFETY: self and g are live; C may realloc and return a different us_socket_t* - unsafe { NonNull::new(c::us_socket_adopt(self, g, k as u8, old_ext, new_ext)) } + unsafe { + NonNull::new(c::us_socket_adopt( + self.as_mut_ptr(), + g, + k as u8, + old_ext, + new_ext, + )) + } } /// `adopt` + attach a fresh `SSL*` from `ssl_ctx` (refcounted by the C @@ -275,7 +284,7 @@ impl us_socket_t { /// `us_socket_upgrade_to_tls` / `wrapTLS`. // TODO: take `self` by value — it is consumed/invalidated; the returned ptr may be a different allocation pub fn adopt_tls( - &mut self, + &self, g: &mut SocketGroup, k: SocketKind, ssl_ctx: &mut SslCtx, @@ -288,7 +297,7 @@ impl us_socket_t { // realloc and return a different us_socket_t* unsafe { NonNull::new(c::us_socket_adopt_tls( - self, + self.as_mut_ptr(), g, k as u8, ssl_ctx, @@ -302,14 +311,14 @@ impl us_socket_t { /// Send ClientHello. Separate from `adopt_tls` so the ext slot can be /// repointed before any handshake/close dispatch can fire. - pub fn start_tls_handshake(&mut self) { + pub fn start_tls_handshake(&self) { c::us_socket_start_tls_handshake(self); } /// Feed bytes that were already read off the wire (e.g. a ClientHello the /// plain-TCP layer consumed before the upgrade) through the same decrypt /// path as bytes arriving from the kernel. - pub fn tls_feed(&mut self, data: &[u8]) { + pub fn tls_feed(&self, data: &[u8]) { if data.is_empty() { return; } @@ -324,7 +333,7 @@ impl us_socket_t { // SAFETY: `self` is a live TLS `us_socket_t`; `chunk` is valid for its // length, which fits in an i32 by construction. unsafe { - c::us_socket_tls_feed(self, chunk.as_ptr().cast(), chunk.len() as i32); + c::us_socket_tls_feed(self.as_mut_ptr(), chunk.as_ptr().cast(), chunk.len() as i32); } } } @@ -332,15 +341,15 @@ impl us_socket_t { /// Tee inbound ciphertext to `us_dispatch_ssl_raw_tap` before `SSL_read` /// consumes it, so the `[raw, tls]` pair from `upgradeTLS` can surface /// encrypted bytes to the original net.Socket `data` listener. - pub fn set_ssl_raw_tap(&mut self, enabled: bool) { + pub fn set_ssl_raw_tap(&self, enabled: bool) { c::us_socket_set_ssl_raw_tap(self, enabled as c_int); } - pub fn write(&mut self, data: &[u8]) -> i32 { + pub fn write(&self, data: &[u8]) -> i32 { let rc = unsafe { // SAFETY: data.as_ptr() valid for data.len() bytes c::us_socket_write( - self, + self.as_mut_ptr(), data.as_ptr(), i32::try_from(data.len().min(MAX_I32)).expect("int cast"), ) @@ -350,11 +359,11 @@ impl us_socket_t { } #[cfg(not(windows))] - pub fn write_fd(&mut self, data: &[u8], file_descriptor: Fd) -> i32 { + pub fn write_fd(&self, data: &[u8], file_descriptor: Fd) -> i32 { let rc = unsafe { // SAFETY: data.as_ptr() valid for data.len() bytes; fd is a valid native descriptor c::us_socket_ipc_write_fd( - self, + self.as_mut_ptr(), data.as_ptr(), i32::try_from(data.len().min(MAX_I32)).expect("int cast"), file_descriptor.native(), @@ -371,18 +380,18 @@ impl us_socket_t { rc } #[cfg(windows)] - pub fn write_fd(&mut self, _data: &[u8], _file_descriptor: Fd) -> i32 { + pub fn write_fd(&self, _data: &[u8], _file_descriptor: Fd) -> i32 { // A `compile_error!` here would brick the windows build even with no // callers (it is evaluated at item definition), so use a runtime trap // instead; no current Windows call site. unreachable!("us_socket_t::write_fd is not implemented on Windows") } - pub fn write2(&mut self, first: &[u8], second: &[u8]) -> i32 { + pub fn write2(&self, first: &[u8], second: &[u8]) -> i32 { let rc = unsafe { // SAFETY: both slices valid for their respective lengths c::us_socket_write2( - self, + self.as_mut_ptr(), first.as_ptr(), first.len(), second.as_ptr(), @@ -404,13 +413,13 @@ impl us_socket_t { /// sends on platforms without it). Same closed/shutdown gating and /// partial-write poll handling as `raw_write`. Plain-TCP only by contract: /// raw writes bypass TLS framing. - pub fn raw_writev(&mut self, iov: &[UsIoVec]) -> i32 { + pub fn raw_writev(&self, iov: &[UsIoVec]) -> i32 { bun_core::scoped_log!(uws, "us_socket_raw_writev({:p}, {})", self, iov.len()); // SAFETY: iov entries reference memory owned by the caller for the // duration of this call; the C side only reads them synchronously. unsafe { c::us_socket_raw_writev( - self, + self.as_mut_ptr(), iov.as_ptr(), i32::try_from(iov.len()).expect("int cast"), ) @@ -418,23 +427,23 @@ impl us_socket_t { } /// Bypass TLS — raw bytes to the fd even if `is_tls()`. - pub fn raw_write(&mut self, data: &[u8]) -> i32 { + pub fn raw_write(&self, data: &[u8]) -> i32 { bun_core::scoped_log!(uws, "us_socket_raw_write({:p}, {})", self, data.len()); unsafe { // SAFETY: data.as_ptr() valid for data.len() bytes c::us_socket_raw_write( - self, + self.as_mut_ptr(), data.as_ptr(), i32::try_from(data.len().min(MAX_I32)).expect("int cast"), ) } } - pub fn flush(&mut self) { + pub fn flush(&self) { c::us_socket_flush(self); } - pub fn send_file_needs_more(&mut self) { + pub fn send_file_needs_more(&self) { c::us_socket_sendfile_needs_more(self); } @@ -480,7 +489,7 @@ mod c { // Shims that take a (ptr,len) pair, nullable raw, or transfer ownership // stay unsafe. unsafe extern "C" { - pub(super) safe fn us_socket_get_native_handle(s: &mut us_socket_t) -> *mut c_void; + pub(super) safe fn us_socket_get_native_handle(s: &us_socket_t) -> *mut c_void; pub(super) safe fn us_socket_local_port(s: &us_socket_t) -> i32; pub(super) safe fn us_socket_remote_port(s: &us_socket_t) -> i32; @@ -494,27 +503,23 @@ mod c { buf: *mut u8, length: *mut i32, ); - pub(super) safe fn us_socket_timeout(s: &mut us_socket_t, seconds: c_uint); - pub(super) safe fn us_socket_long_timeout(s: &mut us_socket_t, minutes: c_uint); - pub(super) safe fn us_socket_nodelay(s: &mut us_socket_t, enable: c_int); - pub(super) safe fn us_socket_set_tos(s: &mut us_socket_t, tos: c_int) -> c_int; - pub(super) safe fn us_socket_get_tos(s: &mut us_socket_t) -> c_int; - pub(super) safe fn us_socket_sni_resolve( - s: &mut us_socket_t, - ctx: *mut SslCtx, - error: c_int, - ); + pub(super) safe fn us_socket_timeout(s: &us_socket_t, seconds: c_uint); + pub(super) safe fn us_socket_long_timeout(s: &us_socket_t, minutes: c_uint); + pub(super) safe fn us_socket_nodelay(s: &us_socket_t, enable: c_int); + pub(super) safe fn us_socket_set_tos(s: &us_socket_t, tos: c_int) -> c_int; + pub(super) safe fn us_socket_get_tos(s: &us_socket_t) -> c_int; + pub(super) safe fn us_socket_sni_resolve(s: &us_socket_t, ctx: *mut SslCtx, error: c_int); pub(super) safe fn us_socket_keepalive( - s: &mut us_socket_t, + s: &us_socket_t, enable: c_int, delay: c_uint, ) -> c_int; - pub(super) safe fn us_socket_ext(s: &mut us_socket_t) -> *mut c_void; - pub(super) safe fn us_socket_group(s: &mut us_socket_t) -> *mut SocketGroup; + pub(super) safe fn us_socket_ext(s: &us_socket_t) -> *mut c_void; + pub(super) safe fn us_socket_group(s: &us_socket_t) -> *mut SocketGroup; pub(super) safe fn us_socket_kind(s: &us_socket_t) -> u8; - pub(super) safe fn us_socket_set_kind(s: &mut us_socket_t, kind: u8); - pub(super) safe fn us_socket_set_ssl_raw_tap(s: &mut us_socket_t, enabled: c_int); + pub(super) safe fn us_socket_set_kind(s: &us_socket_t, kind: u8); + pub(super) safe fn us_socket_set_ssl_raw_tap(s: &us_socket_t, enabled: c_int); pub(super) safe fn us_socket_is_tls(s: &us_socket_t) -> i32; pub(super) fn us_socket_write(s: *mut us_socket_t, data: *const u8, length: i32) -> i32; @@ -539,7 +544,7 @@ mod c { ) -> i32; pub(super) fn us_socket_raw_write(s: *mut us_socket_t, data: *const u8, length: i32) -> i32; - pub(super) safe fn us_socket_flush(s: &mut us_socket_t); + pub(super) safe fn us_socket_flush(s: &us_socket_t); pub(super) fn us_socket_open( s: *mut us_socket_t, @@ -547,14 +552,14 @@ mod c { ip: *const u8, ip_length: i32, ) -> *mut us_socket_t; - pub(super) safe fn us_socket_pause(s: &mut us_socket_t); - pub(super) safe fn us_socket_resume(s: &mut us_socket_t); + pub(super) safe fn us_socket_pause(s: &us_socket_t); + pub(super) safe fn us_socket_resume(s: &us_socket_t); pub(super) fn us_socket_close( s: *mut us_socket_t, code: CloseCode, reason: *mut c_void, ) -> *mut us_socket_t; - pub(super) safe fn us_socket_shutdown(s: &mut us_socket_t); + pub(super) safe fn us_socket_shutdown(s: &us_socket_t); pub(super) safe fn us_socket_is_closed(s: &us_socket_t) -> i32; pub(super) fn us_socket_write_check_error( s: &us_socket_t, @@ -562,9 +567,9 @@ mod c { length: i32, fatal_write_error: *mut i32, ) -> i32; - pub(super) safe fn us_socket_shutdown_read(s: &mut us_socket_t); + pub(super) safe fn us_socket_shutdown_read(s: &us_socket_t); pub(super) safe fn us_socket_is_shut_down(s: &us_socket_t) -> i32; - pub(super) safe fn us_socket_sendfile_needs_more(socket: &mut us_socket_t); + pub(super) safe fn us_socket_sendfile_needs_more(socket: &us_socket_t); pub(super) safe fn us_socket_get_fd(s: &us_socket_t) -> LIBUS_SOCKET_DESCRIPTOR; pub(super) safe fn us_socket_verify_error(s: &us_socket_t) -> us_bun_verify_error_t; pub(super) safe fn us_socket_get_error(s: &us_socket_t) -> c_int; @@ -594,7 +599,7 @@ mod c { data: *const c_char, length: i32, ) -> *mut us_socket_t; - pub(super) safe fn us_socket_start_tls_handshake(s: &mut us_socket_t); + pub(super) safe fn us_socket_start_tls_handshake(s: &us_socket_t); } } diff --git a/src/zstd/lib.rs b/src/zstd/lib.rs index 377872c8c131..1c4040a63d0f 100644 --- a/src/zstd/lib.rs +++ b/src/zstd/lib.rs @@ -6,21 +6,34 @@ use bun_core::ZStr; // ─── FFI bindings ───────────────────────────────────────────────────────── // Externs stay in this crate per PORTING.md §FFI: "If your file has externs // and isn't already *_sys, leave them in place". +/// The C object itself. Only the extern declarations in [`c`] name this type; +/// all Rust code uses the owning [`ZSTD_DStream`] handle. +#[allow(non_camel_case_types)] +pub mod sys { + bun_opaque::opaque_ffi! { + /// `ZSTD_DStream` (`typedef ZSTD_DCtx ZSTD_DStream`) — opaque streaming + /// decompression context. `&Self` is ABI-identical to a non-null + /// `ZSTD_DStream*` and carries no `noalias`/`readonly`: zstd mutates the + /// context on every call. + pub struct ZSTD_DStream; + } +} + #[allow(non_camel_case_types, non_snake_case, non_upper_case_globals)] pub mod c { use core::ffi::{c_char, c_int, c_uint, c_ulonglong, c_void}; - // `ZSTD_DStream` — opaque streaming-decompression context (Nomicon FFI pattern). - // - // `UnsafeCell` makes the type `!Freeze` so a `&ZSTD_DStream` does not assert - // immutability of the C-owned state (zstd mutates internally on every call). + // The decompression context lives in `crate::sys`; Rust owns it via the + // `crate::ZSTD_DStream` handle. Re-exported so the externs below can name it. + pub use crate::sys::ZSTD_DStream; + bun_opaque::opaque_ffi! { - pub struct ZSTD_DStream; /// `ZSTD_CCtx` — opaque streaming-compression context. pub struct ZSTD_CCtx; } - /// `typedef ZSTD_DCtx ZSTD_DStream;` — same opaque object. + /// `typedef ZSTD_DCtx ZSTD_DStream;` — same opaque object. Still names the C + /// object, so the `*mut ZSTD_DCtx` externs below are unaffected by the handle. pub(crate) type ZSTD_DCtx = ZSTD_DStream; // C enums passed by value across FFI — model as `c_uint` (their declared @@ -100,11 +113,16 @@ pub mod c { pub(crate) safe fn ZSTD_getErrorName(code: usize) -> *const c_char; pub(crate) safe fn ZSTD_defaultCLevel() -> c_int; + // Mallocs the context and hands back sole ownership, or null. pub(crate) safe fn ZSTD_createDStream() -> *mut ZSTD_DStream; - pub(crate) fn ZSTD_freeDStream(zds: *mut ZSTD_DStream) -> usize; - pub(crate) fn ZSTD_initDStream(zds: *mut ZSTD_DStream) -> usize; + // safe: `ZSTD_freeDStream` is `ZSTD_freeDCtx`. Freeing is not exclusive + // access, so the receiver is `&`, not `&mut`. + pub(crate) safe fn ZSTD_freeDStream(zds: &ZSTD_DStream) -> usize; + pub(crate) safe fn ZSTD_initDStream(zds: &ZSTD_DStream) -> usize; + // NOT `safe fn`: zstd dereferences `output->dst` / `input->src`, and both + // fields are `pub` raw pointers that safe Rust can forge. pub fn ZSTD_decompressStream( - zds: *mut ZSTD_DStream, + zds: &ZSTD_DStream, output: *mut ZSTD_outBuffer, input: *mut ZSTD_inBuffer, ) -> usize; @@ -186,6 +204,55 @@ bun_core::impl_tag_error!(ZstdError); bun_core::named_error_set!(ZstdError); +// `ZSTD_createDStream()` mallocs the context and hands back sole ownership. One +// `ZSTD_DStream` handle owns exactly one such context. +fn free_dstream(zds: &sys::ZSTD_DStream) { + // `ZSTD_freeDStream` is `ZSTD_freeDCtx`; its `size_t` is always 0. + let _ = c::ZSTD_freeDStream(zds); +} + +bun_opaque::foreign_handle! { + /// Owned handle to a C `ZSTD_DStream` (`== ZSTD_DCtx`). + /// + /// `Drop` frees the context. Every method takes `&self`: zstd mutates the + /// context through the same pointer on every call, so there is no `&mut self` + /// to have, and freeing is not exclusive access either. + #[allow(non_camel_case_types)] + pub struct ZSTD_DStream(sys::ZSTD_DStream) via free_dstream; +} + +impl ZSTD_DStream { + /// `ZSTD_createDStream()` + `ZSTD_initDStream()`. + pub fn create() -> core::result::Result { + // SAFETY: `ZSTD_createDStream` transfers the sole ownership unit, or null. + let zds = unsafe { Self::adopt_ptr(c::ZSTD_createDStream()) } + .ok_or(ZstdError::ZstdFailedToCreateInstance)?; + zds.init(); + Ok(zds) + } + + /// Reset the context for the next frame. Designed to be called repeatedly on + /// the same context; needs no cleanup in between. + pub fn init(&self) { + let _ = c::ZSTD_initDStream(self.raw()); + } + + /// One `ZSTD_decompressStream` step: 0 at a frame boundary, otherwise a + /// read-size hint or an error code (test with [`is_error`]). + /// + /// # Safety + /// `output.dst` and `input.src` must be valid for their `size` bytes; zstd + /// dereferences both. + pub unsafe fn decompress_stream( + &self, + output: &mut c::ZSTD_outBuffer, + input: &mut c::ZSTD_inBuffer, + ) -> usize { + // SAFETY: caller contract; both buffer structs are live `&mut`s. + unsafe { c::ZSTD_decompressStream(self.raw(), &raw mut *output, &raw mut *input) } + } +} + /// ZSTD_compress() : /// Compresses `src` content as a single zstd compressed frame into already allocated `dst`. /// NOTE: Providing `dstCapacity >= ZSTD_compressBound(srcSize)` guarantees that zstd will have @@ -331,7 +398,8 @@ pub struct ZstdReaderArrayList<'a> { // We operate on the caller's Vec directly via the `&mut` borrow. pub list_ptr: &'a mut Vec, // `list_allocator` / `allocator` params deleted — global mimalloc. - pub zstd: *mut c::ZSTD_DStream, + // `None` after `end()`, which frees the context there rather than at drop. + zstd: Option, pub state: State, pub total_out: usize, pub total_in: usize, @@ -355,17 +423,12 @@ impl<'a> ZstdReaderArrayList<'a> { list: &'a mut Vec, // list_allocator / allocator params deleted (global mimalloc). ) -> core::result::Result>, ZstdError> { - let zstd = c::ZSTD_createDStream(); - if zstd.is_null() { - return Err(ZstdError::ZstdFailedToCreateInstance); - } - // SAFETY: zstd is a freshly created non-null DStream. - let _ = unsafe { c::ZSTD_initDStream(zstd) }; + let zstd = ZSTD_DStream::create()?; Ok(Box::new(ZstdReaderArrayList { input, list_ptr: list, - zstd, + zstd: Some(zstd), state: State::Uninitialized, total_out: 0, total_in: 0, @@ -375,9 +438,8 @@ impl<'a> ZstdReaderArrayList<'a> { pub fn end(&mut self) { if self.state != State::End { - // SAFETY: self.zstd was created by ZSTD_createDStream and has not been freed - // (guarded by state != End). - let _ = unsafe { c::ZSTD_freeDStream(self.zstd) }; + // Drops the owned context here, at the point the old free happened. + self.zstd = None; self.state = State::End; } } @@ -426,10 +488,13 @@ impl<'a> ZstdReaderArrayList<'a> { pos: 0, }; - // SAFETY: self.zstd is a valid DStream (state != End); in_buf/out_buf point - // into live slices with correct sizes. - let rc = - unsafe { c::ZSTD_decompressStream(self.zstd, &raw mut out_buf, &raw mut in_buf) }; + // SAFETY: in_buf/out_buf point into live slices with correct sizes. + let rc = unsafe { + self.zstd + .as_ref() + .expect("read_all after end()") + .decompress_stream(&mut out_buf, &mut in_buf) + }; if c::ZSTD_isError(rc) != 0 { self.state = State::Error; return Err(ZstdError::ZstdDecompressionError); @@ -460,10 +525,7 @@ impl<'a> ZstdReaderArrayList<'a> { return Ok(()); } // More input available, reset for the next frame - // ZSTD_initDStream() safely resets the stream state without needing cleanup - // It's designed to be called multiple times on the same DStream object - // SAFETY: self.zstd is a valid DStream. - let _ = unsafe { c::ZSTD_initDStream(self.zstd) }; + self.zstd.as_ref().expect("read_all after end()").init(); continue; } @@ -492,12 +554,6 @@ impl<'a> ZstdReaderArrayList<'a> { } } -impl Drop for ZstdReaderArrayList<'_> { - fn drop(&mut self) { - self.end(); - } -} - // ────────────────────────────────────────────────────────────────────────── // StreamingDecoder // ────────────────────────────────────────────────────────────────────────── @@ -508,7 +564,7 @@ impl Drop for ZstdReaderArrayList<'_> { /// per call, so callers can hold the decoder across multiple body chunks /// without lifetime erasure. pub struct StreamingDecoder { - stream: core::ptr::NonNull, + stream: ZSTD_DStream, pub state: State, /// Decompression-bomb guard: `decompress` errors instead of growing the /// output past this many bytes. Defaults to unbounded. @@ -517,12 +573,8 @@ pub struct StreamingDecoder { impl StreamingDecoder { pub fn new() -> core::result::Result { - let stream = core::ptr::NonNull::new(c::ZSTD_createDStream()) - .ok_or(ZstdError::ZstdFailedToCreateInstance)?; - // SAFETY: stream is a freshly created non-null DStream. - let _ = unsafe { c::ZSTD_initDStream(stream.as_ptr()) }; Ok(Self { - stream, + stream: ZSTD_DStream::create()?, state: State::Uninitialized, max_output_size: usize::MAX, }) @@ -575,11 +627,8 @@ impl StreamingDecoder { pos: 0, }; - // SAFETY: stream is a valid DStream (not freed); in_buf/out_buf - // point into live slices with correct sizes. - let rc = unsafe { - c::ZSTD_decompressStream(self.stream.as_ptr(), &raw mut out_buf, &raw mut in_buf) - }; + // SAFETY: in_buf/out_buf point into live slices with correct sizes. + let rc = unsafe { self.stream.decompress_stream(&mut out_buf, &mut in_buf) }; if c::ZSTD_isError(rc) != 0 { self.state = State::Error; return Err(ZstdError::ZstdDecompressionError); @@ -602,8 +651,7 @@ impl StreamingDecoder { return Ok(()); } // More input available — reinitialize for the next frame. - // SAFETY: stream is a valid DStream. - let _ = unsafe { c::ZSTD_initDStream(self.stream.as_ptr()) }; + self.stream.init(); continue; } @@ -623,10 +671,3 @@ impl StreamingDecoder { Ok(()) } } - -impl Drop for StreamingDecoder { - fn drop(&mut self) { - // SAFETY: stream was created by ZSTD_createDStream; freed once here. - let _ = unsafe { c::ZSTD_freeDStream(self.stream.as_ptr()) }; - } -} diff --git a/test/internal/dead-code-escape-limits.json b/test/internal/dead-code-escape-limits.json index ca28f91a5105..95d045111157 100644 --- a/test/internal/dead-code-escape-limits.json +++ b/test/internal/dead-code-escape-limits.json @@ -14,7 +14,7 @@ "src/io/posix_event_loop.rs": 8, "src/jsc/PosixSignalHandle.rs": 6, "src/jsc_macros/lib.rs": 2, - "src/opaque/lib.rs": 5, + "src/opaque/lib.rs": 10, "src/patch/lib.rs": 2, "src/runtime/api/bun/Terminal.rs": 2, "src/runtime/cli/test/ChangedFilesFilter.rs": 1,