Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 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
7 changes: 5 additions & 2 deletions src/jsc/AsyncModule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,8 +372,11 @@ impl Queue {
// `from_field_ptr!` is sound. S017 does not apply: that rule forbids
// widening from a `&mut self`-derived pointer, but `ctx` is a raw
// `*mut` carried from the original allocation.
let vm = unsafe { &mut *bun_core::from_field_ptr!(VirtualMachine, modules, queue) };
vm.enqueue_task_concurrent(task);
let vm: *mut VirtualMachine =
unsafe { bun_core::from_field_ptr!(VirtualMachine, modules, queue) };
// Checked: the wake can fire after a worker VM that owned `queue` was
// freed by terminate(); the pointer arithmetic above performs no read.
let _ = VirtualMachine::try_enqueue_task_concurrent(vm, task);
}

pub fn on_poll(&mut self) {
Expand Down
4 changes: 3 additions & 1 deletion src/jsc/ConcurrentPromiseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ impl<'a, Context: ConcurrentPromiseTaskContext> ConcurrentPromiseTask<'a, Contex
);
// `task` is the live `concurrent_task` field of the heap-allocated
// job; the queue takes ownership of its intrusive `next` link.
event_loop.enqueue_task_concurrent(task);
// `event_loop` may point into a worker VM freed by terminate() while
// the pool task ran — checked enqueue only.
let _ = EventLoop::try_enqueue_task_concurrent(event_loop.as_ptr(), task);
}

/// Frees the heap allocation backing this task.
Expand Down
7 changes: 5 additions & 2 deletions src/jsc/CppTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ impl ConcurrentCppTask {
// `opaque_ref` above proved it non-null and it has not yet been freed — `run` consumes it here.
unsafe { EventLoopTaskNoContext::run(cpp_task) };
if let Some(vm) = maybe_vm {
vm.event_loop_shared().unref_concurrently();
// Checked: runs on the work-pool thread; the creating VM may be a
// worker freed by terminate() while this task ran.
VirtualMachine::try_unref_concurrently(vm.as_ptr());
}
}
}
Expand All @@ -90,7 +92,8 @@ pub(crate) extern "C" fn ConcurrentCppTask__createAndRun(cpp_task: *mut EventLoo
// `EventLoopTaskNoContext` is an `opaque_ffi!` ZST handle; `opaque_ref` is
// the centralised non-null deref proof. C++ just handed it over.
if let Some(vm) = EventLoopTaskNoContext::opaque_ref(cpp_task).get_vm() {
vm.event_loop_shared().ref_concurrently();
// Checked for symmetry with the pool-thread unref in `run_owned`.
VirtualMachine::try_ref_concurrently(vm.as_ptr());
}
WorkPool::schedule_new(ConcurrentCppTask {
cpp_task,
Expand Down
23 changes: 11 additions & 12 deletions src/jsc/JSCScheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use core::ffi::c_int;

use bun_event_loop::{ConcurrentTask::ConcurrentTask, TaskTag, Taskable, task_tag};

use crate::event_loop::{EventLoop, JsTerminated};
use crate::event_loop::JsTerminated;
use crate::virtual_machine::VirtualMachine;

bun_opaque::opaque_ffi! {
Expand Down Expand Up @@ -38,30 +38,29 @@ impl JSCDeferredWorkTask {

#[unsafe(no_mangle)]
pub(crate) extern "C" fn Bun__eventLoop__incrementRefConcurrently(
jsc_vm: &VirtualMachine,
jsc_vm: *mut VirtualMachine,
delta: c_int,
) {
crate::mark_binding!();
// C++ passes a non-null live `VirtualMachine*`; ABI-compatible with `&T`.
// `event_loop_shared()` is the safe accessor over the VM-owned EventLoop.
let event_loop: &EventLoop = jsc_vm.event_loop_shared();
// Checked: called from JSC helper threads, which can outlive a
// terminated worker's VM (the counter of a freed loop needs no balancing).
if delta > 0 {
event_loop.ref_concurrently();
VirtualMachine::try_ref_concurrently(jsc_vm);
} else {
event_loop.unref_concurrently();
VirtualMachine::try_unref_concurrently(jsc_vm);
}
}

#[unsafe(no_mangle)]
pub(crate) extern "C" fn Bun__queueJSCDeferredWorkTaskConcurrently(
jsc_vm: &VirtualMachine,
jsc_vm: *mut VirtualMachine,
task: *mut JSCDeferredWorkTask,
) {
crate::mark_binding!();
// C++ passes a non-null live `VirtualMachine*`; ABI-compatible with `&T`.
let loop_: &EventLoop = jsc_vm.event_loop_shared();
// `create_from` heap-allocates with the auto-delete bit set.
loop_.enqueue_task_concurrent(ConcurrentTask::create_from(task));
// Checked: called from JSC concurrent threads, which can outlive a
// terminated worker's VM. `create_from` heap-allocates with the
// auto-delete bit set (freed by the checked enqueue when the VM is gone).
let _ = VirtualMachine::try_enqueue_task_concurrent(jsc_vm, ConcurrentTask::create_from(task));
}

/// # Safety
Expand Down
24 changes: 15 additions & 9 deletions src/jsc/RuntimeTranspilerStore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,16 +515,22 @@ impl TranspilerJob {

pub(crate) fn dispatch_to_main_thread(&mut self) {
let vm = self.vm;
// SAFETY: vm outlives the job (BACKREF — VM owns the store).
let transpiler_store: *mut RuntimeTranspilerStore =
unsafe { ptr::addr_of_mut!((*vm).transpiler_store) };
let job = NonNull::from(&mut *self);
// SAFETY: queue is concurrent-safe (UnboundedQueue uses atomics).
unsafe { (*transpiler_store).queue.push(job) };
// Another thread may free `self` at any time after .push, so we cannot use it any more.
// SAFETY: vm outlives the job; event_loop() returns the live self-pointer.
unsafe { &*(*vm).event_loop() }
.enqueue_task_concurrent(ConcurrentTask::create_from(transpiler_store));
// Both the store's queue and the event loop live inside the VM
// allocation, which may be a worker VM freed by terminate() while
// this job ran on the pool — touch them only inside `with_live_vm`
// (the registry lock holds off the free). When the VM is gone the
// job is simply dropped on the floor; nothing will ever drain it.
let _ = crate::VirtualMachineRef::with_live_vm(vm, |vm| {
let transpiler_store: *mut RuntimeTranspilerStore =
ptr::addr_of!(vm.transpiler_store).cast_mut();
// SAFETY: queue is concurrent-safe (UnboundedQueue uses atomics).
unsafe { (*transpiler_store).queue.push(job) };
// Another thread may free `*job` at any time after .push, so we
// cannot use it any more.
vm.event_loop_shared()
.enqueue_task_concurrent(ConcurrentTask::create_from(transpiler_store));
Comment thread
robobun marked this conversation as resolved.
});
}

pub(crate) fn run_from_js_thread(&mut self) -> JsResult<()> {
Expand Down
Loading
Loading