diff --git a/src/bun_alloc/heap_breakdown.rs b/src/bun_alloc/heap_breakdown.rs index 08250c628b2b..c15d1c727ea7 100644 --- a/src/bun_alloc/heap_breakdown.rs +++ b/src/bun_alloc/heap_breakdown.rs @@ -30,11 +30,11 @@ impl Zone { /// `'static`: `malloc_set_zone_name` may keep the pointer instead of copying. #[cfg(target_os = "macos")] pub fn init(name: &'static CStr) -> &'static Zone { - // SAFETY: malloc_create_zone is safe to call with (0, 0); returns a - // process-lifetime zone pointer. `name` is NUL-terminated and outlives the - // process. + let zone = malloc_create_zone(0, 0); + assert!(!zone.is_null(), "malloc_create_zone failed"); + // SAFETY: the zone is not null and nothing destroys it. `name` is NUL-terminated and + // outlives the process. unsafe { - let zone = malloc_create_zone(0, 0); malloc_set_zone_name(zone, name.as_ptr()); &*zone } diff --git a/src/io/posix_event_loop.rs b/src/io/posix_event_loop.rs index f556b07da09b..3bfe5f47ea45 100644 --- a/src/io/posix_event_loop.rs +++ b/src/io/posix_event_loop.rs @@ -626,7 +626,11 @@ impl FilePoll { EPOLL::CTL_ADD }; - let ctl = linux::epoll_ctl(watcher_fd, op, fd.native(), Some(&event)); + // SAFETY: `watcher_fd` is the epoll instance of `loop_`, whose dispatch decodes a + // `Pollable`. `deinit` removes the entry, or leaves one that `EPOLLONESHOT` disarmed + // when it fired, which is not reported again. `Store::put` then holds the slot back, + // marked `IgnoreUpdates`, until a tick returns, when no event fetched for it is left. + let ctl = unsafe { linux::epoll_ctl(watcher_fd, op, fd.native(), Some(&event)) }; self.flags.insert(Flags::WasEverRegistered); if let Some(errno) = errno_sys(ctl, sys::Tag::epoll_ctl) { self.deactivate(loop_); @@ -940,7 +944,8 @@ impl FilePoll { { use bun_sys::linux::{self, EPOLL}; // CTL_DEL keys on fd alone, so both directions are removed together. - let ctl = linux::epoll_ctl(watcher_fd, EPOLL::CTL_DEL, fd.native(), None); + // SAFETY: CTL_DEL registers nothing. + let ctl = unsafe { linux::epoll_ctl(watcher_fd, EPOLL::CTL_DEL, fd.native(), None) }; match sys::get_errno(ctl) { sys::E::SUCCESS => {} diff --git a/src/runtime/ffi/ffi_body.rs b/src/runtime/ffi/ffi_body.rs index 1e9dea696cd4..392e523586e7 100644 --- a/src/runtime/ffi/ffi_body.rs +++ b/src/runtime/ffi/ffi_body.rs @@ -1361,10 +1361,12 @@ impl FFI { pub(crate) fn close<'a>(&self, this_value: JsWrapper<'a, Self>, cx: &mut JsScope<'a>) { unsafe extern "C" { /// Type-checks `library` (`dynamicDowncast`) before use. - safe fn Bun__FFI__closeFunctions(global: &JSGlobalObject, library: JSValue) -> bool; + fn Bun__FFI__closeFunctions(global: &JSGlobalObject, library: JSValue) -> bool; } jsc::mark_binding(); - let is_running = Bun__FFI__closeFunctions(cx.global(), this_value.to_js()); + // SAFETY: C++ indexes `functionsValue` unchecked up to its length. It is empty or the array + // that `cc` filled from 0 to its length, which JS never sees. + let is_running = unsafe { Bun__FFI__closeFunctions(cx.global(), this_value.to_js()) }; if is_running { // The running call returns into the compiled code, so leak it instead of letting `do_close` free it. self.shared_state.set(None); diff --git a/src/runtime/webcore/TextDecoder.rs b/src/runtime/webcore/TextDecoder.rs index f2ffcb11533d..2cbaa37df8bd 100644 --- a/src/runtime/webcore/TextDecoder.rs +++ b/src/runtime/webcore/TextDecoder.rs @@ -4,6 +4,7 @@ use crate::webcore::jsc::{self as jsc, JSGlobalObject, JSString, JSValue}; use bun_core::strings; use bun_jsc::HostReturn as _; use bun_jsc::bindgen::{self, JsHeap, JsLocal, JsScope}; +use core::mem::MaybeUninit; use std::collections::TryReserveError; use jsc::StringJsc as _; @@ -496,10 +497,10 @@ pub(crate) extern "C" fn TextDecoder__createForStream( label: JSValue, fatal: bool, ignore_bom: bool, - out_utf8_fast_path: &mut bool, - out_encoding: &mut EncodingLabel, + out_utf8_fast_path: &mut MaybeUninit, + out_encoding: &mut MaybeUninit, ) -> *mut TextDecoder { - *out_utf8_fast_path = false; + out_utf8_fast_path.write(false); let encoding = if label.is_undefined() { EncodingLabel::Utf8 } else { @@ -524,9 +525,9 @@ pub(crate) extern "C" fn TextDecoder__createForStream( } } }; - *out_encoding = encoding; + out_encoding.write(encoding); if matches!(encoding, EncodingLabel::Utf8) && !fatal { - *out_utf8_fast_path = true; + out_utf8_fast_path.write(true); return core::ptr::null_mut(); } bun_core::heap::into_raw(Box::new(TextDecoder { diff --git a/src/spawn/process.rs b/src/spawn/process.rs index 6e5d5a949f27..0cd38f2ea294 100644 --- a/src/spawn/process.rs +++ b/src/spawn/process.rs @@ -3603,12 +3603,17 @@ mod spawn_process_body { } let mut receipts: [libc::kevent; 5] = bun_core::ffi::zeroed(); - match bun_sys::kevent( - kq_fd, - &changes_buf[..changes_len], - &mut receipts[..changes_len], - None, - ) { + // SAFETY: the loop below reads `udata` as a number. The one other reader of `kq_fd`, + // `NoOrphansTracker::killTracked`, throws the events away. + let registered = unsafe { + bun_sys::kevent( + kq_fd, + &changes_buf[..changes_len], + &mut receipts[..changes_len], + None, + ) + }; + match registered { Err(err) => return Some(Err(err)), Ok(_) => {} } @@ -3652,7 +3657,8 @@ mod spawn_process_body { let mut child_exited = false; let mut child_status: Option = None; loop { - let got = match bun_sys::kevent(kq_fd, &[], &mut events[..], None) { + // SAFETY: an empty changelist registers nothing. + let got = match unsafe { bun_sys::kevent(kq_fd, &[], &mut events[..], None) } { Err(err) => return Some(Err(err)), Ok(c) => c, }; diff --git a/src/sys/lib.rs b/src/sys/lib.rs index 88887633ad53..5ea991abacd0 100644 --- a/src/sys/lib.rs +++ b/src/sys/lib.rs @@ -5590,9 +5590,20 @@ pub mod linux { unsafe { libc::ppoll(fds.as_mut_ptr(), fds.len() as _, timeout, sigmask) } } /// `EPOLL_CTL_DEL` takes no `event`. + /// + /// # Safety + /// Whoever waits on `epfd` gets `event.u64` back and may dereference it. It must be what they + /// expect for as long as `fd` is registered, and until they are done with the events that + /// they had fetched by then. #[inline] - pub fn epoll_ctl(epfd: c_int, op: c_int, fd: c_int, event: Option<&epoll_event>) -> c_int { - super::linux_syscall::epoll_ctl(epfd, op, fd, event) + pub unsafe fn epoll_ctl( + epfd: c_int, + op: c_int, + fd: c_int, + event: Option<&epoll_event>, + ) -> c_int { + // SAFETY: the caller's contract. + unsafe { super::linux_syscall::epoll_ctl(epfd, op, fd, event) } } // ── raw syscall thunks ── @@ -7518,8 +7529,13 @@ pub fn kqueue() -> Maybe { /// `kevent()` — slice-wrapped Maybe form. Retries on EINTR. /// Returns the number of events written into `eventlist`. +/// +/// # Safety +/// Whoever waits on `fd` gets the `udata` of each change back and may dereference it. It must be +/// what they expect for as long as the change is registered, and until they are done with the +/// events that they had fetched by then. #[cfg(any(target_os = "macos", target_os = "freebsd"))] -pub fn kevent( +pub unsafe fn kevent( fd: Fd, changelist: &[libc::kevent], eventlist: &mut [libc::kevent], @@ -7527,7 +7543,7 @@ pub fn kevent( ) -> Maybe { loop { // SAFETY: fd is a valid kqueue; slices give exact (ptr,len); timeout - // is either null or a valid timespec. + // is either null or a valid timespec. The caller answers for each `udata`. let rc = unsafe { libc::kevent( fd.native(), diff --git a/src/sys/linux_syscall.rs b/src/sys/linux_syscall.rs index 9c3449f3857b..73cce239b318 100644 --- a/src/sys/linux_syscall.rs +++ b/src/sys/linux_syscall.rs @@ -420,11 +420,19 @@ pub(crate) fn read_raw(fd: i32, buf: &mut [u8]) -> isize { /// (c) synthesizing EINVAL for unknown `op` values ourselves rather than /// letting the kernel reject them. /// Passing the raw quad straight through avoids all three. +/// +/// # Safety +/// As for `linux::epoll_ctl`. #[inline] -pub(crate) fn epoll_ctl(epfd: i32, op: i32, fd: i32, event: Option<&libc::epoll_event>) -> i32 { +pub(crate) unsafe fn epoll_ctl( + epfd: i32, + op: i32, + fd: i32, + event: Option<&libc::epoll_event>, +) -> i32 { let event = event.map_or(core::ptr::null(), core::ptr::from_ref); // SAFETY: the kernel validates `epfd`/`op`/`fd`; `event` is live, or null, which is - // EFAULT for every `op` but CTL_DEL. + // EFAULT for every `op` but CTL_DEL. The caller answers for `event.u64`. unsafe { libc::syscall(libc::SYS_epoll_ctl, epfd, op, fd, event) as i32 } } diff --git a/src/uws_sys/Loop.rs b/src/uws_sys/Loop.rs index c428570afdd1..d6f4c3d29d4c 100644 --- a/src/uws_sys/Loop.rs +++ b/src/uws_sys/Loop.rs @@ -239,12 +239,17 @@ impl PosixLoop { pub fn drain_quic_if_necessary(&self) { if !self.internal_loop_data.nq_head.get().is_null() { // Full pass with close dispatch deferred to the next loop point. - c::us_nq_loop_drain(self); + // SAFETY: `self` is this thread's live loop. The walk holds its successor while JS + // runs, and relies on an endpoint staying rooted until its close is delivered. + unsafe { c::us_nq_loop_drain(self) }; } if !self.internal_loop_data.has_quic() { return; } - c::us_quic_loop_flush_if_pending(self); + // SAFETY: `self` is this thread's live loop. `processing` keeps this out of every lsquic + // callback but the `on_stream_open` that `us_quic_socket_make_stream` makes, and that one + // does not flush. + unsafe { c::us_quic_loop_flush_if_pending(self) }; } /// `None` if epoll/kqueue cannot be created (EMFILE). @@ -262,18 +267,25 @@ impl PosixLoop { } pub fn tick(&self) { - c::us_loop_run_bun_tick(self, None, NOW_NS_UNKNOWN); + // SAFETY: as in `tick_with_timeout`. + unsafe { c::us_loop_run_bun_tick(self, None, NOW_NS_UNKNOWN) }; } pub fn tick_without_idle(&self) { - c::us_loop_run_bun_tick(self, Some(&Timespec { sec: 0, nsec: 0 }), NOW_NS_UNKNOWN); + // SAFETY: as in `tick_with_timeout`. + unsafe { + c::us_loop_run_bun_tick(self, Some(&Timespec { sec: 0, nsec: 0 }), NOW_NS_UNKNOWN) + }; } /// `now_ns` is the CLOCK_MONOTONIC reading the caller took to pick `timespec` (see /// `timer::All::get_timeout`), reused by the JS park hook's idle-sweep rate limit rather /// than read again. `NOW_NS_UNKNOWN` if the caller has none to share. pub fn tick_with_timeout(&self, timespec: Option<&Timespec>, now_ns: u64) { - c::us_loop_run_bun_tick(self, timespec, now_ns); + // SAFETY: `self` is this thread's live loop, and `jsc_vm` is null or its live VM. A handler + // may tick (`tick_depth`), but that refills the receive buffer and can dispatch the same + // socket again: such a handler is done with what the dispatch lent it. + unsafe { c::us_loop_run_bun_tick(self, timespec, now_ns) }; } /// Free everything queued on `loop->data.closed_head` / @@ -298,7 +310,10 @@ impl PosixLoop { /// loop — covers Listener/App-owned groups that `RareData`'s static field /// list doesn't enumerate. Returns whether any group was linked. pub fn close_all_groups(&self) -> bool { - c::us_loop_close_all_groups(self) != 0 + // SAFETY: `self` is this thread's live loop. The walk reads its successor after the close + // handlers ran, so none may free another group. The one caller runs at teardown, with the + // owners of the groups stopped and script forbidden. + unsafe { c::us_loop_close_all_groups(self) != 0 } } pub fn should_enable_date_header_timer(&self) -> bool { @@ -393,22 +408,29 @@ impl WindowsLoop { /// Windows the park hook is driven from `us_loop_run` (libuv.c), which reads libuv's /// already-refreshed clock via `uv_now` rather than taking one of its own. pub fn tick_with_timeout(&self, _: Option<&Timespec>, _now_ns: u64) { - c::us_loop_run(self); + // SAFETY: as in `run`. + unsafe { c::us_loop_run(self) }; } pub fn tick_without_idle(&self) { - c::us_loop_pump(self); + // SAFETY: as in `run`. + unsafe { c::us_loop_pump(self) }; } pub fn drain_quic_if_necessary(&self) { if !self.internal_loop_data.nq_head.get().is_null() { // Full pass with close dispatch deferred to the next loop point. - c::us_nq_loop_drain(self); + // SAFETY: `self` is this thread's live loop. The walk holds its successor while JS + // runs, and relies on an endpoint staying rooted until its close is delivered. + unsafe { c::us_nq_loop_drain(self) }; } if !self.internal_loop_data.has_quic() { return; } - c::us_quic_loop_flush_if_pending(self); + // SAFETY: `self` is this thread's live loop. `processing` keeps this out of every lsquic + // callback but the `on_stream_open` that `us_quic_socket_make_stream` makes, and that one + // does not flush. + unsafe { c::us_quic_loop_flush_if_pending(self) }; } /// `None` if `uv_loop_init` fails (handle exhaustion). @@ -421,7 +443,10 @@ impl WindowsLoop { } pub fn run(&self) { - c::us_loop_run(self); + // SAFETY: `self` is this thread's live loop, and `jsc_vm` is null or its live VM. A handler + // may tick (`tick_depth`, `poll_cb_depth`), though libuv does not provide for `uv_run` in a + // callback: such a handler is done with what the dispatch lent it, its handle included. + unsafe { c::us_loop_run(self) }; } // TODO: remove these two aliases @@ -464,7 +489,10 @@ impl WindowsLoop { } pub fn close_all_groups(&self) -> bool { - c::us_loop_close_all_groups(self) != 0 + // SAFETY: `self` is this thread's live loop. The walk reads its successor after the close + // handlers ran, so none may free another group. The one caller runs at teardown, with the + // owners of the groups stopped and script forbidden. + unsafe { c::us_loop_close_all_groups(self) != 0 } } pub fn update_date(&self) { @@ -508,21 +536,21 @@ mod c { ext_size: c_uint, ) -> *mut Loop; pub(super) fn us_loop_free(loop_: *mut Loop); - pub(super) safe fn us_quic_loop_flush_if_pending(loop_: &Loop); - pub(super) safe fn us_nq_loop_drain(loop_: &Loop); + pub(super) fn us_quic_loop_flush_if_pending(loop_: &Loop); + pub(super) fn us_nq_loop_drain(loop_: &Loop); #[cfg(windows)] - pub(super) safe fn us_loop_run(loop_: &Loop); + pub(super) fn us_loop_run(loop_: &Loop); #[cfg(windows)] - pub(super) safe fn us_loop_pump(loop_: &Loop); + pub(super) fn us_loop_pump(loop_: &Loop); pub fn us_wakeup_loop(loop_: *const Loop); #[cfg(not(windows))] - pub(super) safe fn us_loop_run_bun_tick( + pub(super) fn us_loop_run_bun_tick( loop_: &Loop, timeout_ms: Option<&Timespec>, now_ns: u64, ); pub(super) fn us_internal_free_closed_sockets(loop_: &Loop); - pub(super) safe fn us_loop_close_all_groups(loop_: &Loop) -> c_int; + pub(super) fn us_loop_close_all_groups(loop_: &Loop) -> c_int; #[cfg(not(windows))] pub(super) safe fn uws_get_loop() -> *mut Loop; #[cfg(windows)] diff --git a/src/watcher/KEventWatcher.rs b/src/watcher/KEventWatcher.rs index c784b8636584..a8ce212b23f8 100644 --- a/src/watcher/KEventWatcher.rs +++ b/src/watcher/KEventWatcher.rs @@ -56,7 +56,8 @@ pub(crate) fn watch_loop_cycle(this: &mut Watcher) -> bun_sys::Result<()> { let mut changelist: [libc::kevent; CHANGELIST_COUNT] = bun_core::ffi::zeroed(); - let mut count = bun_sys::kevent(fd, &[], &mut changelist, None)?; + // SAFETY: an empty changelist registers nothing. + let mut count = unsafe { bun_sys::kevent(fd, &[], &mut changelist, None) }?; // Give the events more time to coalesce if count < CHANGELIST_COUNT / 2 { @@ -64,7 +65,8 @@ pub(crate) fn watch_loop_cycle(this: &mut Watcher) -> bun_sys::Result<()> { tv_sec: 0, tv_nsec: 100_000, }; // 0.0001 seconds - count += bun_sys::kevent(fd, &[], &mut changelist[count..], Some(&ts))?; + // SAFETY: as above. + count += unsafe { bun_sys::kevent(fd, &[], &mut changelist[count..], Some(&ts)) }?; } let changes = &changelist[..count]; diff --git a/src/watcher/Watcher.rs b/src/watcher/Watcher.rs index ce05b8b677d8..41ac25504180 100644 --- a/src/watcher/Watcher.rs +++ b/src/watcher/Watcher.rs @@ -517,7 +517,8 @@ impl Watcher { // Basically: // - We register the event here. // our while(true) loop above receives notification of changes to any of the events created here. - let _ = bun_sys::kevent(self.platform.fd, &[event], &mut [], None); + // SAFETY: only `watch_loop_cycle` waits on this kqueue, and it reads `udata` as an index. + let _ = unsafe { bun_sys::kevent(self.platform.fd, &[event], &mut [], None) }; } fn append_file_assume_capacity(