Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
64 changes: 42 additions & 22 deletions packages/bun-usockets/src/eventing/epoll_kqueue.c
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,20 @@ struct us_loop_t *us_create_loop(void *hint, void (*wakeup_cb)(struct us_loop_t
#else
loop->fd = kqueue();
#endif
/* EMFILE/ENFILE on epoll_create1/kqueue, or on the wakeup eventfd inside
* us_internal_loop_data_init: return NULL so per-call loop creation
* (Bun.spawnSync's SpawnSyncEventLoop) can surface a catchable error
* instead of aborting the process. */
if (loop->fd == -1) {
us_free(loop);
return NULL;
}

us_internal_loop_data_init(loop, wakeup_cb, pre_cb, post_cb);
if (us_internal_loop_data_init(loop, wakeup_cb, pre_cb, post_cb) != 0) {
close(loop->fd);
us_free(loop);
return NULL;
}
return loop;
}

Expand Down Expand Up @@ -697,10 +709,15 @@ struct us_internal_async *us_internal_create_async(struct us_loop_t *loop, int f

int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
if (efd == -1) {
// eventfd only fails on EMFILE/ENFILE — the loop is unusable without
// wakeup_async, and the sole caller doesn't NULL-check. Crash loudly
// rather than NULL-deref or store -1 as a poll fd.
BUN_PANIC("eventfd() failed during loop init (out of file descriptors?)");
/* EMFILE/ENFILE: the loop is unusable without its wakeup async.
* Return NULL so us_internal_loop_data_init (and in turn
* us_create_loop) can unwind and let the caller surface a catchable
* error instead of taking the process down. */
if (!fallthrough) {
loop->num_polls--;
}
us_free(p);
return NULL;
Comment thread
robobun marked this conversation as resolved.
}
us_poll_init(p, efd, POLL_TYPE_CALLBACK);

Expand Down Expand Up @@ -774,26 +791,29 @@ struct us_internal_async *us_internal_create_async(struct us_loop_t *loop, int f
mach_port_t self = mach_task_self();
kern_return_t kr = mach_port_allocate(self, MACH_PORT_RIGHT_RECEIVE, &cb->port);

if (UNLIKELY(kr != KERN_SUCCESS)) {
return NULL;
}

// Insert a send right into the port since we also use this to send
kr = mach_port_insert_right(self, cb->port, cb->port, MACH_MSG_TYPE_MAKE_SEND);
if (UNLIKELY(kr != KERN_SUCCESS)) {
return NULL;
if (kr == KERN_SUCCESS) {
// Insert a send right into the port since we also use this to send
kr = mach_port_insert_right(self, cb->port, cb->port, MACH_MSG_TYPE_MAKE_SEND);
if (kr == KERN_SUCCESS) {
// Modify the port queue size to be 1 because we are only
// using it for notifications and not for any other purpose.
mach_port_limits_t limits = { .mpl_qlimit = 1 };
kr = mach_port_set_attributes(self, cb->port, MACH_PORT_LIMITS_INFO, (mach_port_info_t)&limits, MACH_PORT_LIMITS_INFO_COUNT);
if (kr == KERN_SUCCESS) {
return (struct us_internal_async *) cb;
}
}
/* Dropping the receive right destroys the port; any send right it
* carried becomes a dead name that the failing caller never uses. */
mach_port_mod_refs(self, cb->port, MACH_PORT_RIGHT_RECEIVE, -1);
Comment thread
robobun marked this conversation as resolved.
Outdated
}

// Modify the port queue size to be 1 because we are only
// using it for notifications and not for any other purpose.
mach_port_limits_t limits = { .mpl_qlimit = 1 };
kr = mach_port_set_attributes(self, cb->port, MACH_PORT_LIMITS_INFO, (mach_port_info_t)&limits, MACH_PORT_LIMITS_INFO_COUNT);

if (UNLIKELY(kr != KERN_SUCCESS)) {
return NULL;
if (!fallthrough) {
loop->num_polls--;
}

return (struct us_internal_async *) cb;
us_free(cb->machport_buf);
us_free(cb);
return NULL;
}

// identical code as for timer, make it shared for "callback types"
Expand Down
11 changes: 10 additions & 1 deletion packages/bun-usockets/src/eventing/libuv.c
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,15 @@ struct us_loop_t *us_create_loop(void *hint,
(struct us_loop_t *)us_calloc(1, sizeof(struct us_loop_t) + ext_size);

loop->uv_loop = hint ? hint : uv_loop_new();
/* uv_loop_new() returns NULL when uv_loop_init fails (CreateIoCompletionPort
* under handle/non-paged-pool exhaustion on Windows). Without this check the
* uv_prepare_init below dereferences NULL and segfaults the process; the
* only hint==NULL caller is Bun.spawnSync's per-call isolated loop, so the
* failure must surface as a thrown error instead. */
if (!loop->uv_loop) {
us_free(loop);
return NULL;
}
loop->is_default = hint != 0;

loop->uv_pre = us_malloc(sizeof(uv_prepare_t));
Expand All @@ -283,7 +292,7 @@ struct us_loop_t *us_create_loop(void *hint,
loop->uv_check->data = loop;

// here we create two unreffed handles - timer and async
us_internal_loop_data_init(loop, wakeup_cb, pre_cb, post_cb);
(void)us_internal_loop_data_init(loop, wakeup_cb, pre_cb, post_cb);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// if we do not own this loop, we need to integrate and set up timer
if (hint) {
Expand Down
8 changes: 4 additions & 4 deletions packages/bun-usockets/src/internal/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,10 @@ void us_internal_group_maybe_unlink(struct us_socket_group_t *group);
* SSL path calls _raw once it's actually time to drop the fd. */
struct us_socket_t *us_internal_socket_close_raw(us_socket_r s, int code, void *reason);
struct us_socket_t *us_internal_ssl_close(us_socket_r s, int code, void *reason);
void us_internal_loop_data_init(struct us_loop_t *loop,
void (*wakeup_cb)(us_loop_r loop),
void (*pre_cb)(us_loop_r loop),
void (*post_cb)(us_loop_r loop));
int us_internal_loop_data_init(struct us_loop_t *loop,
void (*wakeup_cb)(us_loop_r loop),
void (*pre_cb)(us_loop_r loop),
void (*post_cb)(us_loop_r loop));
void us_internal_loop_data_free(us_loop_r loop);
void us_internal_loop_pre(us_loop_r loop);
void us_internal_loop_post(us_loop_r loop);
Expand Down
14 changes: 13 additions & 1 deletion packages/bun-usockets/src/loop.c
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ void us_internal_sweep_if_due(struct us_loop_t *loop) {
#endif


void us_internal_loop_data_init(struct us_loop_t *loop, void (*wakeup_cb)(struct us_loop_t *loop),
/* Returns 0 on success, -1 when the wakeup async cannot be created
* (eventfd/mach_port under resource exhaustion). On failure nothing is left
* allocated in loop->data. */
int us_internal_loop_data_init(struct us_loop_t *loop, void (*wakeup_cb)(struct us_loop_t *loop),
void (*pre_cb)(struct us_loop_t *loop), void (*post_cb)(struct us_loop_t *loop)) {
// We allocate with calloc, so we only need to initialize the specific fields in use.
#ifdef LIBUS_USE_LIBUV
Expand All @@ -136,12 +139,21 @@ void us_internal_loop_data_init(struct us_loop_t *loop, void (*wakeup_cb)(struct
loop->data.pre_cb = pre_cb;
loop->data.post_cb = post_cb;
loop->data.wakeup_async = us_internal_create_async(loop, 1, 0);
if (!loop->data.wakeup_async) {
us_free(loop->data.recv_buf);
us_free(loop->data.send_buf);
#ifdef LIBUS_USE_LIBUV
us_timer_close(loop->data.sweep_timer, 0);
#endif
return -1;
}
us_internal_async_set(loop->data.wakeup_async, (void (*)(struct us_internal_async *)) wakeup_cb);
#if ASSERT_ENABLED
if (Bun__lock__size != sizeof(loop->data.mutex)) {
BUN_PANIC("The size of the mutex must match the size of the lock");
}
#endif
return 0;
}

void us_internal_loop_data_free(struct us_loop_t *loop) {
Expand Down
16 changes: 13 additions & 3 deletions packages/bun-uws/src/Loop.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,11 @@ struct Loop {
}

static Loop *create(void *hint) {
Loop *loop = ((Loop *) us_create_loop(hint, wakeupCb, preCb, postCb, sizeof(LoopData)))->init();
return loop;
Loop *loop = (Loop *) us_create_loop(hint, wakeupCb, preCb, postCb, sizeof(LoopData));
if (!loop) {
return nullptr;
}
return loop->init();
}

/* What to do with loops created with existingNativeLoop? */
Expand Down Expand Up @@ -119,7 +122,14 @@ struct Loop {
getLazyLoop().loop = create(existingNativeLoop);
/* We cannot register automatic free here, must be manually done */
} else {
getLazyLoop().loop = create(nullptr);
Loop *loop = create(nullptr);
if (!loop) {
/* Resource exhaustion (epoll/kqueue/eventfd EMFILE, or
* uv_loop_init). Leave lazyLoop null so a later get()
* retries once resources free up. */
return nullptr;
}
getLazyLoop().loop = loop;
getLazyLoop().cleanMe = true;
}
}
Expand Down
19 changes: 12 additions & 7 deletions src/event_loop/SpawnSyncEventLoop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,15 +146,18 @@ impl SpawnSyncEventLoop {
// below, so `Self` MUST NOT move after `init` returns (no-move invariant
// upheld by the caller). The caller provides uninitialized storage, hence
// `MaybeUninit<Self>` (out-param ctor exception).
//
// Returns `false` when the isolated event loop cannot be created
// (uv_loop_init / epoll_create1 / kqueue failure under resource
// exhaustion). `this` is left uninitialized on failure.
pub fn init(
this: &mut core::mem::MaybeUninit<Self>,
vm: *mut (), /* SAFETY: erased *mut VirtualMachine */
) {
) -> bool {
// `uws::Loop::create` takes a `LoopHandler` impl with associated-const fn ptrs.
let loop_ = uws::Loop::create::<handler::Handler>();

let loop_ =
NonNull::new(loop_).expect("uws::Loop::create never returns null (asserts on OOM)");
let Some(loop_) = uws::Loop::create::<handler::Handler>() else {
return false;
};
Comment thread
robobun marked this conversation as resolved.

// Initialize the JSC EventLoop with empty state.
// CRITICAL: On Windows, the impl stores our isolated loop pointer in `uws_loop`.
Expand Down Expand Up @@ -183,6 +186,7 @@ impl SpawnSyncEventLoop {
let loop_data = &mut this.uws_loop_mut().internal_loop_data;
loop_data.set_parent_raw(tag, ptr);
loop_data.jsc_vm = core::ptr::null();
true
}

/// Erased `*mut bun_jsc::event_loop::EventLoop` (heap-owned via
Expand All @@ -201,8 +205,9 @@ impl SpawnSyncEventLoop {
/// Shared borrow of the isolated `uws::Loop`.
///
/// # Safety (invariant)
/// `uws_loop` is created in `init` via `uws::Loop::create` (asserts
/// non-null) and freed only in `Drop`, so it is valid for all of `self`'s
/// `uws_loop` is created in `init` via `uws::Loop::create`; `init` returns
/// `false` on `None`, so `Self` is never constructed with a null loop. It
/// is freed only in `Drop`, so it is valid for all of `self`'s
/// lifetime. The loop is only mutated through `&mut self` paths
/// (`uws_loop_mut`), so a shared borrow tied to `&self` cannot overlap a
/// unique borrow.
Expand Down
21 changes: 16 additions & 5 deletions src/jsc/rare_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -705,20 +705,31 @@ impl RareData {
.push(CleanupHook::from(global_this, ctx, func));
}

pub fn spawn_sync_event_loop(&mut self, vm: &mut VirtualMachine) -> &mut SpawnSyncEventLoop {
/// Returns `None` when the isolated event loop cannot be created
/// (uv_loop_init / epoll_create1 / kqueue failure under resource
/// exhaustion); the caller surfaces that as a thrown JS error so a
/// `Bun.spawnSync` under resource pressure fails the one call instead of
/// taking the process down. Once created the loop is cached, so only the
/// first call per VM can return `None`.
pub fn spawn_sync_event_loop(
&mut self,
vm: &mut VirtualMachine,
) -> Option<&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::<SpawnSyncEventLoop>::new_uninit();
SpawnSyncEventLoop::init(
if !SpawnSyncEventLoop::init(
&mut *boxed,
core::ptr::from_mut::<VirtualMachine>(vm).cast::<()>(),
);
// SAFETY: `init` fully initialised the slot.
) {
return None;
}
// SAFETY: `init` fully initialised the slot when it returned `true`.
self.spawn_sync_event_loop_ = Some(unsafe { boxed.assume_init() });
}
self.spawn_sync_event_loop_.as_mut().unwrap()
self.spawn_sync_event_loop_.as_deref_mut()
}

pub fn mime_type_from_string(&mut self, str_: &[u8]) -> Option<mime_type::MimeType> {
Expand Down
31 changes: 28 additions & 3 deletions src/runtime/api/bun/js_bun_spawn_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1075,9 +1075,16 @@
// SAFETY: see note above; `spawn_sync_event_loop` re-borrows the
// same VM via the raw pointer for its `vm` arg.
unsafe {
let sync_loop = (*jsc_vm_ptr)
let Some(sync_loop) = (*jsc_vm_ptr)
.rare_data()
.spawn_sync_event_loop(&mut *jsc_vm_ptr);
.spawn_sync_event_loop(&mut *jsc_vm_ptr)
else {
// The per-spawnSync isolated event loop could not be created
// (uv_loop_init on Windows, epoll_create1/kqueue on POSIX).
// Throw instead of letting the unchecked NULL dereference in
// us_create_loop segfault the whole process.
return Err(throw_spawn_sync_loop_init_failed(global_this));
};
Comment thread
robobun marked this conversation as resolved.
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
Expand All @@ -1104,6 +1111,7 @@
(*jsc_vm_ptr_cleanup)
.rare_data()
.spawn_sync_event_loop(&mut *jsc_vm_ptr_cleanup)
.expect("cached by the IS_SYNC prepare above")
.cleanup(jsc_vm_ptr_cleanup.cast(), main_loop.cast());
}
}
Expand Down Expand Up @@ -1908,7 +1916,8 @@
// SAFETY: jsc_vm_ptr is the live thread VM; re-borrowed for the nested arg.
let sync_loop = unsafe { &mut *jsc_vm_ptr }
.rare_data()
.spawn_sync_event_loop(unsafe { &mut *jsc_vm_ptr });
.spawn_sync_event_loop(unsafe { &mut *jsc_vm_ptr })
.expect("cached by the IS_SYNC prepare above");

while subprocess.compute_has_pending_activity() {
// Re-evaluate this at each iteration of the loop since it may change between iterations.
Expand Down Expand Up @@ -2081,6 +2090,22 @@
Ok(sync_value)
}

fn throw_spawn_sync_loop_init_failed(global_this: &JSGlobalObject) -> JsError {
let err = SystemError {
message: BunString::static_(
b"spawnSync failed to initialize its event loop (system resource exhaustion)",
),
code: BunString::static_("EMFILE"),
errno: -UV_E::MFILE,
path: BunString::EMPTY,
syscall: BunString::static_("uv_loop_init"),
hostname: BunString::EMPTY,
fd: -1,
dest: BunString::EMPTY,
};
global_this.throw_value(err.to_error_instance(global_this))
}

Check warning on line 2107 in src/runtime/api/bun/js_bun_spawn_bindings.rs

View check run for this annotation

Claude / Claude Code Review

Hardcoded EMFILE / uv_loop_init in spawnSync loop-init error is inaccurate on POSIX

The hardcoded `syscall: "uv_loop_init"` and `code: "EMFILE"` are inaccurate on POSIX — the failing syscall there is `epoll_create1`/`kqueue`/`eventfd` (libuv isn't in the picture), and the errno may be `ENFILE` rather than `EMFILE`. Since `us_create_loop` discards the real errno by returning `NULL`, the true cause can't be recovered here — but a Linux/macOS user seeing `syscall: "uv_loop_init"` will be misled. Consider `cfg`-gating the syscall string per platform, or leaving `syscall` empty (as
Comment thread
robobun marked this conversation as resolved.

fn throw_command_not_found(global_this: &JSGlobalObject, command: &[u8]) -> JsError {
let err = SystemError {
message: BunString::create_format(format_args!(
Expand Down
15 changes: 9 additions & 6 deletions src/uws_sys/Loop.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use core::ffi::{c_int, c_uint, c_void};
use core::ptr::NonNull;

use crate::InternalLoopData;
use crate::Timespec;
Expand Down Expand Up @@ -229,13 +230,14 @@ impl PosixLoop {
unsafe { c::us_quic_loop_flush_if_pending(self) };
}

pub fn create<H: LoopHandler>() -> *mut Loop {
/// Returns `None` when the kernel cannot create the backing event
/// provider (epoll/kqueue EMFILE); callers surface that as an error.
pub fn create<H: LoopHandler>() -> Option<NonNull<Loop>> {
// SAFETY: us_create_loop allocates and returns a new loop; null hint is valid
let p = unsafe {
c::us_create_loop(core::ptr::null_mut(), Some(H::WAKEUP), H::PRE, H::POST, 0)
};
assert!(!p.is_null(), "us_create_loop returned null");
p
NonNull::new(p)
}

pub fn wakeup(&mut self) {
Expand Down Expand Up @@ -470,13 +472,14 @@ impl WindowsLoop {
unsafe { c::us_quic_loop_flush_if_pending(self) };
}

pub fn create<H: LoopHandler>() -> *mut WindowsLoop {
/// Returns `None` when `uv_loop_init` fails (CreateIoCompletionPort under
/// handle/non-paged-pool exhaustion); callers surface that as an error.
pub fn create<H: LoopHandler>() -> Option<NonNull<WindowsLoop>> {
// SAFETY: us_create_loop allocates and returns a new loop; null hint is valid
let p = unsafe {
c::us_create_loop(core::ptr::null_mut(), Some(H::WAKEUP), H::PRE, H::POST, 0)
};
assert!(!p.is_null(), "us_create_loop returned null");
p
NonNull::new(p)
}

pub fn run(&mut self) {
Expand Down
Loading
Loading