Skip to content
Closed
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f2311f4
Don't enqueue to a terminated worker's freed event loop from other th…
robobun Jun 10, 2026
c618e4c
Allow not_unsafe_ptr_arg_deref on the checked enqueue entry points
robobun Jun 10, 2026
007f64b
Address review: dangling-reference fields, failure-path cleanup, visi…
robobun Jun 10, 2026
64bafc1
Don't deinit a dead worker's FetchTasklet against its freed JSC heap
robobun Jun 10, 2026
575acf4
Make cross-thread VM field reads atomic and shrink dead-VM tasklet pa…
robobun Jun 10, 2026
3ca7b27
Publish MAIN_THREAD_VM only once the main VM is fully initialized
robobun Jun 10, 2026
0cf6132
Carry a generation token with cross-thread VM handles (#32082)
robobun Jun 12, 2026
4bbd5de
Merge branch 'main' into farm/7117067b/worker-terminate-concurrent-qu…
Jarred-Sumner Jun 12, 2026
c417173
Make VmHandle the only cross-thread VM identity, including C++ captures
robobun Jun 12, 2026
950d3c9
Remove a no-op borrow of the fetch tasklet's VM handle
robobun Jun 12, 2026
26fe733
Load the positive-delivery worker from a file instead of a data: URL
robobun Jun 12, 2026
6f4c56a
Sync stale VM-lifetime comments with the handle semantics
robobun Jun 12, 2026
a300ceb
Fix two more stale lifetime comments and ungroup the parked-tasklet s…
robobun Jun 12, 2026
26aa62e
ci: retrigger
robobun Jun 12, 2026
c34de46
Merge main into worker-terminate-concurrent-queue-uaf
robobun Jul 3, 2026
7ffec34
Merge remote-tracking branch 'origin/main' into farm/7117067b/worker-…
robobun Jul 10, 2026
1192b82
ci: re-run checks
robobun Jul 10, 2026
1e6d30a
Merge remote-tracking branch 'origin/main' into farm/7117067b/worker-…
robobun Jul 10, 2026
d1c2a20
Correct stale doc claims about MAIN_THREAD_VM publish, event_loop tar…
robobun Jul 10, 2026
7f78581
Fix three more doc comments contradicted by the generation capture
robobun Jul 10, 2026
76cb8a3
Qualify the link_impl_JsEventLoop header for the cross-thread enqueue…
robobun Jul 10, 2026
d680781
Merge remote-tracking branch 'origin/main' into farm/7117067b/worker-…
robobun Jul 11, 2026
99fb435
Merge remote-tracking branch 'origin/main' into farm/7117067b/worker-…
robobun Jul 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/bundler/ParseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2809,14 +2809,15 @@ pub mod parse_worker {
.any_loop_mut()
.expect("BundleV2.linker.loop must be set before scheduling ParseTask")
{
bun_event_loop::AnyEventLoop::Js { owner } => {
bun_event_loop::AnyEventLoop::Js { owner, generation } => {
owner.enqueue_task_concurrent(
bun_event_loop::ConcurrentTask::ConcurrentTask::from_callback(result, |p| {
// SAFETY: `p` is the `result` Box leaked above; ownership
// transfers to `on_complete`, which deallocates it.
unsafe { on_complete(p) };
Ok(())
}),
*generation,
);
}
bun_event_loop::AnyEventLoop::Mini(mini) => {
Expand Down
3 changes: 2 additions & 1 deletion src/bundler/ServerComponentParseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,14 +121,15 @@ fn task_callback_wrap(thread_pool_task: *mut ThreadPoolTask) {
.any_loop_mut()
.expect("BundleV2.linker.loop must be set before scheduling ServerComponentParseTask")
{
bun_event_loop::AnyEventLoop::Js { owner } => {
bun_event_loop::AnyEventLoop::Js { owner, generation } => {
owner.enqueue_task_concurrent(
bun_event_loop::ConcurrentTask::ConcurrentTask::from_callback(result, |p| {
// SAFETY: `p` is the `result` Box leaked above; ownership
// transfers to `on_complete`, which deallocates it.
unsafe { on_complete(p) };
Ok(())
}),
*generation,
);
}
bun_event_loop::AnyEventLoop::Mini(mini) => {
Expand Down
10 changes: 6 additions & 4 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1565,8 +1565,8 @@ pub mod bv2_impl {
// the plugins.
// `any_loop_mut` centralises the BACKREF deref of `linker.r#loop`.
match &*self.any_loop_mut() {
bun_event_loop::AnyEventLoop::Js { owner } => {
owner.enqueue_task_concurrent(task);
bun_event_loop::AnyEventLoop::Js { owner, generation } => {
owner.enqueue_task_concurrent(task, *generation);
}
bun_event_loop::AnyEventLoop::Mini(_) => {
panic!("No JavaScript event loop for transpiler plugins to run on");
Expand Down Expand Up @@ -4204,12 +4204,13 @@ pub mod bv2_impl {
// `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() {
bun_event_loop::AnyEventLoop::Js { owner } => {
bun_event_loop::AnyEventLoop::Js { owner, generation } => {
owner.enqueue_task_concurrent(
bun_event_loop::ConcurrentTask::ConcurrentTask::from_callback(
std::ptr::from_mut(load),
on_load_from_js_loop_raw,
),
*generation,
);
}
bun_event_loop::AnyEventLoop::Mini(mini) => {
Expand All @@ -4229,12 +4230,13 @@ 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() {
bun_event_loop::AnyEventLoop::Js { owner } => {
bun_event_loop::AnyEventLoop::Js { owner, generation } => {
owner.enqueue_task_concurrent(
bun_event_loop::ConcurrentTask::ConcurrentTask::from_callback(
std::ptr::from_mut(resolve),
on_resolve_from_js_loop_raw,
),
*generation,
);
}
bun_event_loop::AnyEventLoop::Mini(mini) => {
Expand Down
109 changes: 78 additions & 31 deletions src/event_loop/AnyEventLoop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,22 @@
unsafe { JsEventLoop::new(JsEventLoopKind::Jsc, js_event_loop) }
}

/// Registration generation of a live JS event loop, captured at handle
/// construction and carried next to the erased pointer so the cross-thread
/// `enqueue_task_concurrent` dispatch can be validated against the live-VM
/// registry even after the loop (a terminated worker's) is freed. 0 for the
/// null "never dispatched" placeholder handle.
#[inline]
fn js_loop_generation(owner: JsEventLoop) -> u64 {
if owner.owner.is_null() {
0
} else {
// The dispatch dereferences the loop — sound here because every
// handle constructor requires the loop to be live at construction.
owner.live_generation()
}
}

/// Useful for code that may need an event loop and could be used from either JavaScript or directly without JavaScript.
/// Unlike jsc.EventLoopHandle, this owns the event loop when it's not a JavaScript event loop.
// Variant order/discriminant must match `crate::EventLoopKind`.
Expand All @@ -54,6 +70,12 @@
/// `link_interface!` invariant ("owner is live for every dispatch") is
/// established once at construction; dispatch is safe.
owner: JsEventLoop,
/// Registration generation captured with `owner` — see
/// [`js_loop_generation`]. Carried by the cross-thread
/// `enqueue_task_concurrent` dispatch, which is the one method that
/// must tolerate `owner` pointing at a freed (terminated worker)
/// loop.
generation: u64,
},
Mini(Box<MiniEventLoop<'a>>),
}
Expand All @@ -73,7 +95,7 @@
impl<'a> AnyEventLoop<'a> {
pub fn iteration_number(&self) -> u64 {
match self {
AnyEventLoop::Js { owner } => owner.iteration_number(),
AnyEventLoop::Js { owner, .. } => owner.iteration_number(),
// SAFETY: see `MiniEventLoop::loop_ptr()` invariant.
AnyEventLoop::Mini(mini) => unsafe { (*mini.loop_ptr()).iteration_number() },
}
Expand All @@ -97,20 +119,26 @@
/// `js_event_loop` is a live erased `*mut jsc::EventLoop`
/// that outlives every dispatch through the returned
/// `AnyEventLoop`. The pointer is not dereferenced here — it's stored
/// opaquely in [`JsEventLoop`] and only dereferenced at dispatch sites.
/// opaquely in [`JsEventLoop`] and only dereferenced at dispatch sites
/// (the generation capture reads it once, while it is live per the
/// constructor contract).
#[inline]
pub fn js(js_event_loop: *mut ()) -> AnyEventLoop<'static> {
let owner = jsc_event_loop_handle(js_event_loop);
AnyEventLoop::Js {
owner: jsc_event_loop_handle(js_event_loop),
owner,
generation: js_loop_generation(owner),
}
}

/// Construct the `Js` variant for the current thread's JS event loop.
/// Replaces `jsc::VirtualMachine::get().event_loop()` for tier-≤4 callers
/// (e.g. `bun_install::PackageManager`).
pub fn js_current() -> AnyEventLoop<'static> {
let owner = JsEventLoop::current();
AnyEventLoop::Js {
owner: JsEventLoop::current(),
owner,
generation: js_loop_generation(owner),
}
}

Expand All @@ -122,7 +150,7 @@
is_done: fn(*mut core::ffi::c_void) -> bool,
) {
match self {
AnyEventLoop::Js { owner } => {
AnyEventLoop::Js { owner, .. } => {
while !is_done(context) {
owner.tick();
owner.auto_tick();
Expand Down Expand Up @@ -158,7 +186,7 @@
// returns; the borrow ends at the bottom of this loop body before
// the next `is_done` call.
match unsafe { &mut *this } {
AnyEventLoop::Js { owner } => {
AnyEventLoop::Js { owner, .. } => {
owner.tick();
owner.auto_tick();
}
Expand All @@ -176,7 +204,7 @@

pub fn tick_once(&mut self, context: *mut core::ffi::c_void) {
match self {
AnyEventLoop::Js { owner } => {
AnyEventLoop::Js { owner, .. } => {
let _ = context;
owner.tick();
owner.auto_tick_active();
Expand Down Expand Up @@ -284,6 +312,9 @@
/// [`AnyEventLoop::Js`]. `JsEventLoop` is `Copy`, so the handle stays
/// `Copy`.
owner: JsEventLoop,
/// Registration generation captured with `owner` — see
/// [`js_loop_generation`] and [`AnyEventLoop::Js`].
generation: u64,
},
// `BackRef<MiniEventLoop>` (not `&mut`) because the handle is `Copy` and
// stored in `uws::InternalLoopData` as a non-owning backref.
Expand Down Expand Up @@ -369,8 +400,10 @@
/// that are overwritten before use).
#[inline]
pub fn init(js_event_loop: *mut ()) -> EventLoopHandle {
let owner = jsc_event_loop_handle(js_event_loop);
EventLoopHandle::Js {
owner: jsc_event_loop_handle(js_event_loop),
owner,
generation: js_loop_generation(owner),

Check warning on line 406 in src/event_loop/AnyEventLoop.rs

View check run for this annotation

Claude / Claude Code Review

EventLoopHandle::init / from_tag_ptr docs still say 'not dereferenced here' after js_loop_generation was added

Two more constructor docs of the same class the d1c2a20b sweep fixed elsewhere: `EventLoopHandle::init` at L396-398 still says "The pointer is not dereferenced here — it's stored opaquely … and only dereferenced at dispatch sites", and `from_tag_ptr`'s `# Safety` at L460-461 still says "The constructor itself only stores the opaque pointer" — but both now call `js_loop_generation(owner)`, which dereferences the loop. The inner body SAFETY at L474-476 *was* updated ("which also licenses the gener
Comment thread
robobun marked this conversation as resolved.
}
}

Expand All @@ -395,7 +428,7 @@
// which is what the `EventLoopCtxKind::Js` `link_impl_EventLoopCtx!`
// (in `bun_jsc`) is written for. Both are per-thread singletons
// that outlive the ctx.
EventLoopHandle::Js { owner } => unsafe {
EventLoopHandle::Js { owner, .. } => unsafe {
bun_io::EventLoopCtx::new(bun_io::EventLoopCtxKind::Js, owner.bun_vm())
},
// `mini` is a `BackRef` to the live per-thread singleton (see
Expand Down Expand Up @@ -435,12 +468,18 @@
ptr: *mut core::ffi::c_void,
) -> EventLoopHandle {
match tag {
1 => EventLoopHandle::Js {
1 => {
// SAFETY: `(tag, ptr)` was produced by `into_tag_ptr` on a
// still-live event loop, so `ptr` is a live erased
// `*mut jsc::EventLoop`. Same boundary as `EventLoopHandle::init`.
owner: unsafe { JsEventLoop::new(JsEventLoopKind::Jsc, ptr.cast::<()>()) },
},
// `*mut jsc::EventLoop`. Same boundary as `EventLoopHandle::init`
// (which also licenses the generation read in
// `js_loop_generation`).
let owner = unsafe { JsEventLoop::new(JsEventLoopKind::Jsc, ptr.cast::<()>()) };
EventLoopHandle::Js {
owner,
generation: js_loop_generation(owner),
}
}
// `(tag, ptr)` came from `into_tag_ptr` on a live loop, so `ptr`
// is non-null. `BackRef: From<NonNull<T>>`.
2 => EventLoopHandle::Mini(NonNull::new(ptr.cast()).expect("non-null mini ptr").into()),
Expand Down Expand Up @@ -471,59 +510,64 @@

pub fn from_any(any: &mut AnyEventLoop<'static>) -> EventLoopHandle {
match any {
AnyEventLoop::Js { owner } => EventLoopHandle::Js { owner: *owner },
AnyEventLoop::Js { owner, generation } => EventLoopHandle::Js {
owner: *owner,
generation: *generation,
},
AnyEventLoop::Mini(mini) => EventLoopHandle::Mini(BackRef::new_mut(&mut **mini)),
}
}

/// `EventLoopHandle` for the current thread's JS event loop. Replaces
/// `jsc::EventLoopHandle.init(jsc::VirtualMachine.get())` for tier-≤4 callers.
pub fn js_current() -> EventLoopHandle {
let owner = JsEventLoop::current();
EventLoopHandle::Js {
owner: JsEventLoop::current(),
owner,
generation: js_loop_generation(owner),
}
}

/// Erased `*mut jsc::JSGlobalObject` or null (Mini has no JS global).
pub fn global_object(self) -> *mut () {
match self {
EventLoopHandle::Js { owner } => owner.global_object(),
EventLoopHandle::Js { owner, .. } => owner.global_object(),
EventLoopHandle::Mini(_) => core::ptr::null_mut(),
}
}

/// Erased `*mut jsc::VirtualMachine` or null.
pub fn bun_vm(self) -> *mut () {
match self {
EventLoopHandle::Js { owner } => owner.bun_vm(),
EventLoopHandle::Js { owner, .. } => owner.bun_vm(),
EventLoopHandle::Mini(_) => core::ptr::null_mut(),
}
}

/// Erased `*mut webcore::blob::Store`.
pub fn stdout(self) -> *mut () {
match self {
EventLoopHandle::Js { owner } => owner.stdout(),
EventLoopHandle::Js { owner, .. } => owner.stdout(),
EventLoopHandle::Mini(mut mini) => mini_mut(&mut mini).stdout(),
}
}

/// Erased `*mut webcore::blob::Store`.
pub fn stderr(self) -> *mut () {
match self {
EventLoopHandle::Js { owner } => owner.stderr(),
EventLoopHandle::Js { owner, .. } => owner.stderr(),
EventLoopHandle::Mini(mut mini) => mini_mut(&mut mini).stderr(),
}
}

pub fn enter(self) {
if let EventLoopHandle::Js { owner } = self {
if let EventLoopHandle::Js { owner, .. } = self {
owner.enter();
}
}

pub fn exit(self) {
if let EventLoopHandle::Js { owner } = self {
if let EventLoopHandle::Js { owner, .. } = self {
owner.exit();
}
}
Expand All @@ -542,7 +586,7 @@
/// for the brief region they need `&mut`.
pub fn file_polls(self) -> *mut bun_io::file_poll::Store {
match self {
EventLoopHandle::Js { owner } => owner.file_polls(),
EventLoopHandle::Js { owner, .. } => owner.file_polls(),
EventLoopHandle::Mini(mut mini) => std::ptr::from_mut(mini_mut(&mut mini).file_polls()),
}
}
Expand All @@ -557,7 +601,7 @@
match self {
// `JsEventLoop::put_file_poll` takes a raw `*mut FilePoll`; pass
// the decayed `poll_ptr` straight through.
EventLoopHandle::Js { owner } => {
EventLoopHandle::Js { owner, .. } => {
owner.put_file_poll(poll_ptr.as_ptr(), was_ever_registered)
}
// ctx only touches `after_event_loop_callback{,_ctx}`, field-disjoint
Expand All @@ -573,11 +617,14 @@

pub fn enqueue_task_concurrent(self, task: EventLoopTaskPtr) {
match self {
EventLoopHandle::Js { owner } => {
EventLoopHandle::Js { owner, generation } => {
// SAFETY: caller guarantees `task.js` is the active union member
// when `self` is `Js`, and points at a live `ConcurrentTask`
// (non-null).
owner.enqueue_task_concurrent(unsafe { NonNull::new_unchecked(task.js) })
// (non-null). The dispatch validates `(owner, generation)` against
// the live-VM registry, so a freed (terminated worker) loop
// drops the task instead of being dereferenced.
owner
.enqueue_task_concurrent(unsafe { NonNull::new_unchecked(task.js) }, generation)
}
EventLoopHandle::Mini(mut mini) => {
// SAFETY: caller guarantees `task.mini` is the active union
Expand All @@ -591,7 +638,7 @@

pub fn r#loop(self) -> *mut UwsLoop {
match self {
EventLoopHandle::Js { owner } => owner.uws_loop(),
EventLoopHandle::Js { owner, .. } => owner.uws_loop(),
// `loop_ptr` takes `&self`; safe via `BackRef: Deref`.
EventLoopHandle::Mini(mini) => mini.loop_ptr(),
}
Expand Down Expand Up @@ -630,7 +677,7 @@
/// Same `Copy`-handle aliasing concern as [`file_polls`].
pub fn pipe_read_buffer(self) -> *mut [u8] {
match self {
EventLoopHandle::Js { owner } => owner.pipe_read_buffer(),
EventLoopHandle::Js { owner, .. } => owner.pipe_read_buffer(),
EventLoopHandle::Mini(mut mini) => {
std::ptr::from_mut::<[u8]>(mini_mut(&mut mini).pipe_read_buffer())
}
Expand All @@ -649,7 +696,7 @@

pub fn env(self) -> *mut DotEnvLoader<'static> {
match self {
EventLoopHandle::Js { owner } => owner.env(),
EventLoopHandle::Js { owner, .. } => owner.env(),
// `env` must be set — caller invariant. `env_ptr()` takes
// `&self` and returns `Option<NonNull<DotEnvLoader>>` (mutable
// provenance). Safe via `BackRef: Deref`.
Expand All @@ -664,7 +711,7 @@
pub fn top_level_dir(self) -> &'static [u8] {
match self {
// SAFETY: slice borrowed for VM lifetime.
EventLoopHandle::Js { owner } => unsafe { &*owner.top_level_dir() },
EventLoopHandle::Js { owner, .. } => unsafe { &*owner.top_level_dir() },
// SAFETY: `BackRef::get()` ties the borrow to the local `mini`, but
// the pointee is the per-thread singleton (process-lifetime); widen
// to `'static` so the return type matches the Js arm.
Expand All @@ -676,7 +723,7 @@
self,
) -> Result<bun_dotenv::NullDelimitedEnvMap, bun_core::AllocError> {
match self {
EventLoopHandle::Js { owner } => owner.create_null_delimited_env_map(),
EventLoopHandle::Js { owner, .. } => owner.create_null_delimited_env_map(),
EventLoopHandle::Mini(mini) => {
// `env_ptr()` takes `&self` — safe via `BackRef: Deref`.
// `env` must be set (caller invariant).
Expand Down
3 changes: 2 additions & 1 deletion src/event_loop/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ bun_dispatch::link_interface! {
fn enter();
fn exit();
fn enqueue_task(task: Task);
fn enqueue_task_concurrent(task: core::ptr::NonNull<ConcurrentTask::ConcurrentTask>);
fn enqueue_task_concurrent(task: core::ptr::NonNull<ConcurrentTask::ConcurrentTask>, generation: u64);
fn live_generation() -> u64;
fn env() -> *mut bun_dotenv::Loader<'static>;
fn top_level_dir() -> *const [u8];
fn create_null_delimited_env_map() -> Result<bun_dotenv::NullDelimitedEnvMap, bun_core::AllocError>;
Expand Down
Loading
Loading