Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
46 changes: 36 additions & 10 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,41 @@ impl VirtualMachine {
self.rare_data.as_mut().unwrap()
}

/// Drain every socket group linked into the per-VM uSockets loop. Must run
/// BEFORE JSC teardown: `close_all_groups` fires `on_close` → JS callbacks →
/// needs a live VM. `RareData`'s `Drop` runs after `WebWorker__teardownJSCVM`
/// and only `deinit()`s (asserts empty in debug).
///
/// Takes `&self` because it only touches the uSockets loop (a separate heap
/// allocation reached via `uws_loop_mut`), not any `VirtualMachine` field.
pub fn close_all_socket_groups(&self) {
// closeAll() dispatches on_close into JS while the VM is still alive, so a
// handler can call Bun.connect/postgres/etc. and re-populate a group we
// just drained. Loop until every group is observed empty in the same pass
// (bounded: each retry only happens if a JS callback opened a new socket,
// and the cap stops a deliberately-spinning on_close from wedging
// teardown; the post-close force-drain in close_all handles whatever's
// left after the cap).
// Walk the loop's linked-group list rather than RareData's 14 embedded
// fields: Listener/uWS-App groups own their own SocketGroup, and accepted
// sockets land *there*, not in RareData. Iterating only the embedded
// fields missed those, leaking one 88-byte us_socket_t per still-open
// accepted connection at process.exit() (the LSAN cluster on #29932
// build 49245).
Comment thread
coderabbitai[bot] marked this conversation as resolved.
let mut rounds: u8 = 0;
while rounds < 8 {
if !self.uws_loop_mut().close_all_groups() {
break;
}
rounds += 1;
}
// us_socket_close pushes to loop->data.closed_head; loop_post() normally
// frees it on the next tick. We're past the last tick, so drain it now or
// every us_socket_t (libc-allocated) becomes an LSAN leak once we
// unregister the RareData root region.
self.uws_loop_mut().drain_closed_sockets();
}

pub fn is_main_thread(&self) -> bool {
self.worker.is_none()
}
Expand Down Expand Up @@ -1560,16 +1595,7 @@ impl VirtualMachine {
// alive (closeAll() fires on_close → JS). After JSC teardown,
// RareData's Drop only deinit()s the groups (asserts empty).
if self.rare_data.is_some() {
// Note: reshaped for borrowck — `close_all_socket_groups`
// walks the loop's group list via `vm.uws_loop()` and never
// touches `vm.rare_data`, so the disjoint reborrow is sound.
// SAFETY: `self` is the live per-thread VM; the shared borrow
// only reads `event_loop_handle` (no overlap with `rare_data`).
let vm_ref = unsafe { &*core::ptr::from_ref(self) };
self.rare_data
.as_deref_mut()
.unwrap()
.close_all_socket_groups(vm_ref);
self.close_all_socket_groups();
}
// Destroy the per-VM c-ares channel while JSC / `RareData.file_polls`
// / `runtime_state` are all still live — `ares_destroy()` re-enters
Expand Down
30 changes: 17 additions & 13 deletions src/jsc/hot_reloader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -860,11 +860,6 @@ where
let hashes = slice.items_hash();
let parents = slice.items_parent_hash();
let file_descriptors = slice.items_fd();
// Note: reshaped for borrowck — `ctx` is held as a raw pointer so
// `self` can be reborrowed inside the loop body for tombstone access,
// and so the deferred `flush_evictions` doesn't hold `&mut Watcher`
// across the loop.
let ctx: *mut Watcher = std::ptr::from_mut(self.get_context());
// Wrap the Task itself in a guard so any exit path (including future
// early-returns) flushes the buffered hashes via `enqueue()`.
// Dereferenced as `&mut *current_task` for the loop body below.
Expand All @@ -881,14 +876,18 @@ where
Task::<Ctx, EventLoopType, RELOAD_IMMEDIATELY>::init_empty(self),
|mut t| t.enqueue(),
);
// See the note above for why this drops *before* `current_task`.
let _flush = scopeguard::guard(ctx, |ctx| {
// See the note above for why this drops *before* `current_task`. The
// guard captures the `BackRef<Ctx>` (Copy) so the Watcher is reached on
// drop without holding a `&mut` across the loop body.
let _flush = scopeguard::guard(self.ctx, |mut ctx_ref| {
Output::flush();
// SAFETY: the Watcher outlives this call (it owns the Reloader that calls us).
unsafe { (*ctx).flush_evictions() };
// SAFETY: BACKREF invariant — `ctx` outlives the reloader; at guard
// drop no other `&mut Ctx` borrow is live (the loop body's short
// `self.get_context()` reborrows have all retired).
unsafe { ctx_ref.get_mut() }
.bun_watcher_mut()
.flush_evictions();
});
// SAFETY: the Watcher outlives this call (it owns the Reloader that calls us).
let ctx = unsafe { &mut *ctx };

let fs: &mut FileSystem = FileSystem::instance();
let rfs: &mut Fs::file_system::RealFS = &mut fs.fs;
Expand Down Expand Up @@ -916,7 +915,12 @@ where
if event.op.contains(WatchOp::DELETE)
|| (event.op.contains(WatchOp::RENAME) && IS_KQUEUE)
{
ctx.remove_at_index(bun_watcher::Kind::File, event.index, 0, &[]);
self.get_context().remove_at_index(
bun_watcher::Kind::File,
event.index,
0,
&[],
);
}

if self.verbose {
Expand Down Expand Up @@ -1202,7 +1206,7 @@ where
)
));
}
ctx.remove_at_index(
self.get_context().remove_at_index(
bun_watcher::Kind::File,
entry_id as u16,
0,
Expand Down
37 changes: 0 additions & 37 deletions src/jsc/rare_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -830,43 +830,6 @@ impl RareData {
loop_,
)
}

// ── close_all_socket_groups ───────────────────────────────────────────
/// Drain every embedded socket group. Must run BEFORE JSC teardown — closeAll
/// fires on_close → JS callbacks → needs a live VM. RareData.deinit() runs
/// after `WebWorker__teardownJSCVM`, so doing the closeAll
/// there would dispatch into freed JSC heap.
pub fn close_all_socket_groups(&mut self, vm: &VirtualMachine) {
// closeAll() dispatches on_close into JS while the VM is still alive, so a
// handler can call Bun.connect/postgres/etc. and re-populate a group we
// just drained. Loop until every group is observed empty in the same pass
// (bounded — each retry only happens if a JS callback opened a *new*
// socket, and the cap stops a deliberately-spinning on_close from wedging
// teardown; the post-close force-drain in close_all handles whatever's
// left after the cap).
// Walk the loop's linked-group list rather than just our 14 embedded
// fields: Listener/uWS-App groups own their own SocketGroup, and accepted
// sockets land *there*, not in RareData. Iterating only the embedded
// fields missed those, leaking one 88-byte us_socket_t per still-open
// accepted connection at process.exit() (the LSAN cluster on #29932
// build 49245).
let _ = self;
let mut rounds: u8 = 0;
while rounds < 8 {
// `uws_loop_mut()` is the centralised BACKREF accessor for the
// per-VM uSockets loop (live for the VM lifetime).
if !vm.uws_loop_mut().close_all_groups() {
break;
}
rounds += 1;
}
// us_socket_close pushes to loop->data.closed_head; loop_post() normally
// frees it on the next tick. We're past the last tick, so drain it now —
// every us_socket_t is libc-allocated and otherwise becomes an LSAN leak
// (the only pointer into it lives in mimalloc-backed RareData, which LSAN
// can't trace once we unregister the root region).
vm.uws_loop_mut().drain_closed_sockets();
}
}

// ──────────────────────────────────────────────────────────────────────────
Expand Down
11 changes: 2 additions & 9 deletions src/jsc/web_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1265,15 +1265,8 @@ impl WebWorker {
// Embedded socket groups must drain while JSC is still alive —
// closeAll() fires on_close → JS callbacks. RareData.deinit() runs
// after teardownJSCVM and only deinit()s (asserts empty in debug).
if let Some(rare) = vm.rare_data.as_deref_mut() {
// reshaped for borrowck — `close_all_socket_groups`
// wants `&VirtualMachine` while `rare` is `&mut` borrowed from
// `vm`. Re-derive `vm` through the raw ptr (sole owner).

// SAFETY: `vm_ptr` was unpublished under `vm_lock` above, so this
// thread is the sole owner; the JSC VM is still alive (teardown
// is step 3 below).
rare.close_all_socket_groups(unsafe { &*vm_ptr });
if vm.rare_data.is_some() {
vm.close_all_socket_groups();
}
// Destroy the per-VM c-ares channel now: `ares_destroy()` fires
// every pending query callback with `ARES_EDESTRUCTION` and then
Expand Down
Loading