diff --git a/packages/bun-usockets/src/loop.c b/packages/bun-usockets/src/loop.c index 586e8232d3c0..5f557376992e 100644 --- a/packages/bun-usockets/src/loop.c +++ b/packages/bun-usockets/src/loop.c @@ -23,7 +23,9 @@ #include #include #ifndef WIN32 +#include #include +#include #endif #ifdef __linux__ #include @@ -693,16 +695,41 @@ void us_internal_dispatch_ready_poll(struct us_poll_t *p, int error, int eof, in msg.msg_iovlen = 1; msg.msg_name = NULL; msg.msg_namelen = 0; - msg.msg_controllen = CMSG_LEN(sizeof(int)); + /* CMSG_SPACE (the full aligned buffer), or FreeBSD truncates and drops the fd. */ + msg.msg_controllen = sizeof(cmsg_buf); msg.msg_control = cmsg_buf; + // Received descriptors must not leak into children we spawn. + #ifdef MSG_CMSG_CLOEXEC + length = bsd_recvmsg(us_poll_fd(&s->p), &msg, recv_flags | MSG_CMSG_CLOEXEC); + #else length = bsd_recvmsg(us_poll_fd(&s->p), &msg, recv_flags); + #endif - // Extract file descriptor if present + // Extract the file descriptor if present. One per message is the + // protocol; close anything else a peer packed into the buffer. if (length > 0 && msg.msg_controllen > 0) { - struct cmsghdr *cmsg_ptr = CMSG_FIRSTHDR(&msg); - if (cmsg_ptr && cmsg_ptr->cmsg_level == SOL_SOCKET && cmsg_ptr->cmsg_type == SCM_RIGHTS) { - int fd = *(int *)CMSG_DATA(cmsg_ptr); + int fd = -1; + for (struct cmsghdr *cmsg_ptr = CMSG_FIRSTHDR(&msg); cmsg_ptr; cmsg_ptr = CMSG_NXTHDR(&msg, cmsg_ptr)) { + if (cmsg_ptr->cmsg_level != SOL_SOCKET || cmsg_ptr->cmsg_type != SCM_RIGHTS || cmsg_ptr->cmsg_len < CMSG_LEN(0)) { + continue; + } + unsigned char *fds = CMSG_DATA(cmsg_ptr); + size_t nfds = (cmsg_ptr->cmsg_len - CMSG_LEN(0)) / sizeof(int); + for (size_t i = 0; i < nfds; i++) { + int received; + memcpy(&received, fds + i * sizeof(int), sizeof(int)); + #ifndef MSG_CMSG_CLOEXEC + fcntl(received, F_SETFD, FD_CLOEXEC); + #endif + if (fd == -1) { + fd = received; + } else { + close(received); + } + } + } + if (fd != -1) { s = us_dispatch_fd(s, fd); if (!s || us_socket_is_closed(s)) { break; diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index a696dc668c12..521adec31725 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "caad865eb1a6e5ca4427f5ea1f066140b11953e7"; +export const WEBKIT_VERSION = "687eb8e1b73cb2d45ea9e689a97ea8cb867ab754"; /** * WebKit (JavaScriptCore) — the JS engine. diff --git a/scripts/build/flags.ts b/scripts/build/flags.ts index d1ab66b6c8d6..da67305be6ee 100644 --- a/scripts/build/flags.ts +++ b/scripts/build/flags.ts @@ -1386,7 +1386,7 @@ export const linkerFlags: Flag[] = [ "-Wl,--build-id=sha1", ], when: c => c.freebsd, - desc: "FreeBSD linker tuning (same as Linux ELF)", + desc: "FreeBSD linker tuning (same as Linux ELF; here -z stack-size also sizes the main thread's stack)", }, { // rust-lang/llvm-project doesn't enable `LLVM_ENABLE_ZLIB` (or `_ZSTD`) for diff --git a/scripts/runner.node.mjs b/scripts/runner.node.mjs index 9ef779b2ad96..5f02c5a4807f 100755 --- a/scripts/runner.node.mjs +++ b/scripts/runner.node.mjs @@ -54,6 +54,7 @@ import { getSecret, getShell, getWindowsExitReason, + isAndroid, isBuildkite, isCI, isGithubAction, @@ -184,7 +185,7 @@ const { values: options, positionals: filters } = parseArgs({ }, ["coredump-upload"]: { type: "boolean", - default: isBuildkite && isLinux, + default: isBuildkite && isLinux && !isAndroid, }, ["parallel"]: { type: "boolean", diff --git a/scripts/utils.mjs b/scripts/utils.mjs index a1a204c09e7b..cc7bc6f5d078 100755 --- a/scripts/utils.mjs +++ b/scripts/utils.mjs @@ -21,8 +21,11 @@ import { normalize as normalizeWindows } from "node:path/win32"; export const isWindows = process.platform === "win32"; export const isMacOS = process.platform === "darwin"; -export const isLinux = process.platform === "linux"; -export const isPosix = isMacOS || isLinux; +// Node built for Termux/bionic reports "android"; CI models that as linux + abi=android. +export const isAndroid = process.platform === "android"; +export const isLinux = process.platform === "linux" || isAndroid; +export const isFreeBSD = process.platform === "freebsd"; +export const isPosix = isMacOS || isLinux || isFreeBSD; export const isArm64 = process.arch === "arm64"; export const isX64 = process.arch === "x64"; @@ -1538,15 +1541,18 @@ export function parseNumber(value) { /** * @param {string} string - * @returns {"darwin" | "linux" | "windows"} + * @returns {"darwin" | "linux" | "windows" | "freebsd"} */ export function parseOs(string) { if (/darwin|apple|mac/i.test(string)) { return "darwin"; } - if (/linux/i.test(string)) { + if (/linux|android/i.test(string)) { return "linux"; } + if (/freebsd/i.test(string)) { + return "freebsd"; + } if (/win/i.test(string)) { return "windows"; } @@ -1554,7 +1560,7 @@ export function parseOs(string) { } /** - * @returns {"darwin" | "linux" | "windows"} + * @returns {"darwin" | "linux" | "windows" | "freebsd"} */ export function getOs() { return parseOs(process.platform); @@ -1604,13 +1610,17 @@ export function getKernel() { } /** - * @returns {"musl" | "gnu" | undefined} + * @returns {"musl" | "gnu" | "android" | undefined} */ export function getAbi() { if (!isLinux) { return; } + if (isAndroid || existsSync("/system/bin/linker64")) { + return "android"; + } + if (existsSync("/etc/alpine-release")) { return "musl"; } diff --git a/src/bun_core/lib.rs b/src/bun_core/lib.rs index 9d4fe108d449..995f78bab6be 100644 --- a/src/bun_core/lib.rs +++ b/src/bun_core/lib.rs @@ -2406,19 +2406,15 @@ pub mod ffi { /// Safe `uname(2)` wrapper: zero-init a `utsname`, call `libc::uname`, return /// it by value. On the (theoretical) error path the struct stays all-zero, /// so every `c_char[]` field reads as an empty NUL-terminated string. + /// Goes through the `libc` crate rather than binding the symbol by name: + /// on FreeBSD the exported `uname` is a compat entry with 32-byte fields and + /// the real call is `__xuname(256, buf)`, which the crate already handles. #[cfg(unix)] #[inline] pub fn uname() -> libc::utsname { - // `&mut libc::utsname` is ABI-identical to libc's `struct utsname *` - // (thin non-null pointer to a `#[repr(C)]` struct); the type encodes - // the only pointer-validity precondition, so `safe fn` discharges the - // link-time proof and the call needs no `unsafe` block. - unsafe extern "C" { - #[link_name = "uname"] - safe fn libc_uname(buf: &mut libc::utsname) -> core::ffi::c_int; - } let mut u: libc::utsname = zeroed(); - let _ = libc_uname(&mut u); + // SAFETY: `u` is a valid, writable utsname for the duration of the call. + let _ = unsafe { libc::uname(&raw mut u) }; u } diff --git a/src/io/lib.rs b/src/io/lib.rs index b5eb529e8c52..82b8e55ea107 100644 --- a/src/io/lib.rs +++ b/src/io/lib.rs @@ -1542,15 +1542,19 @@ impl Poll { ); let one_shot_flag = libc::EV_ONESHOT; - let udata: usize = Pollable::init(tag, std::ptr::from_mut::(poll)).ptr() as usize; - let (filter, flags_): (i16, u16) = match action { - ApplyAction::Readable => (libc::EVFILT_READ, libc::EV_ADD | one_shot_flag), - ApplyAction::Writable => (libc::EVFILT_WRITE, libc::EV_ADD | one_shot_flag), + let owner = Pollable::init(tag, std::ptr::from_mut::(poll)).ptr() as usize; + // A cancel carries no udata: its owner is finished (`on_done` runs right + // after), EV_DELETE matches by (ident, filter) alone, and any receipt for it + // (knote already fired → ENOENT, fd closed → EBADF) must land on the + // `PollableTag::Empty` early return, not on the stale owner. + let (filter, flags_, udata): (i16, u16, usize) = match action { + ApplyAction::Readable => (libc::EVFILT_READ, libc::EV_ADD | one_shot_flag, owner), + ApplyAction::Writable => (libc::EVFILT_WRITE, libc::EV_ADD | one_shot_flag, owner), ApplyAction::Cancel => { if poll.flags.contains(Flags::PollReadable) { - (libc::EVFILT_READ, libc::EV_DELETE) + (libc::EVFILT_READ, libc::EV_DELETE, 0) } else if poll.flags.contains(Flags::PollWritable) { - (libc::EVFILT_WRITE, libc::EV_DELETE) + (libc::EVFILT_WRITE, libc::EV_DELETE, 0) } else { unreachable!() } @@ -1631,8 +1635,8 @@ impl Poll { let pollable = Pollable::from(event.udata as u64); let tag = pollable.tag(); - // The waker is registered with udata=0 → tag=.empty. The wakeup exists - // only to unblock kevent() so the pending queue drains. + // The waker (whose event only exists to unblock kevent() so the pending + // queue drains) and cancels are submitted with udata=0 → tag=.empty. if tag == PollableTag::Empty { return; } @@ -1640,7 +1644,10 @@ impl Poll { // CYCLEBREAK: owner (ReadFile/WriteFile) is T6; dispatch via link-time // `extern "Rust"` defined in `bun_runtime::dispatch`. The // container_of(io_poll) recovery happens there. - if event.flags == libc::EV_ERROR { + // A changelist entry the kernel could not apply comes back with EV_ERROR + // set (xnu ORs it into the action bits, FreeBSD replaces them) and the + // errno in `data`. + if (event.flags & libc::EV_ERROR) != 0 { log!("error({}) = {}", event.ident, event.data); // SAFETY: poll is the `io_poll` field of a live owner; link-time // extern body matches on `tag`. diff --git a/src/runtime/api/bun/Terminal.rs b/src/runtime/api/bun/Terminal.rs index 700eb630396d..827680346d2d 100644 --- a/src/runtime/api/bun/Terminal.rs +++ b/src/runtime/api/bun/Terminal.rs @@ -10,8 +10,6 @@ //! - Callbacks are stored via `values` in classes.ts, accessed via js.gc use core::cell::Cell; -#[cfg(unix)] -use core::ffi::c_ulong; use core::ffi::{c_int, c_void}; #[cfg(windows)] use core::sync::atomic::{AtomicU32, Ordering}; @@ -920,8 +918,9 @@ mod lib_util { #[cfg(unix)] fn get_open_pty_fn() -> Option { - // On macOS, openpty is in libc, so we can use it directly - #[cfg(target_os = "macos")] + // openpty is linked directly on macOS (libc) and FreeBSD (libutil, see + // scripts/build/bun.ts). + #[cfg(any(target_os = "macos", target_os = "freebsd"))] { // Declared locally (not via the `libc` crate) so the `OpenPtyFn` // type unifies with the Linux dlsym path. @@ -947,7 +946,12 @@ fn get_open_pty_fn() -> Option { return lib_util::get_open_pty(); } - #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "android")))] + #[cfg(not(any( + target_os = "macos", + target_os = "linux", + target_os = "android", + target_os = "freebsd" + )))] None } @@ -1565,11 +1569,6 @@ impl Terminal { #[cfg(unix)] { - #[cfg(target_os = "macos")] - const TIOCSWINSZ: c_ulong = 0x80087467; - #[cfg(not(target_os = "macos"))] - const TIOCSWINSZ: c_ulong = 0x5414; - let winsize = bun_core::Winsize { row: new_rows, col: new_cols, @@ -1582,7 +1581,7 @@ impl Terminal { let ioctl_result = unsafe { libc::ioctl( self.master_fd.get().native(), - TIOCSWINSZ as _, + libc::TIOCSWINSZ as _, &raw const winsize, ) }; diff --git a/src/runtime/cli/install_completions_command.rs b/src/runtime/cli/install_completions_command.rs index f3563ed9b30b..6fbc529a1677 100644 --- a/src/runtime/cli/install_completions_command.rs +++ b/src/runtime/cli/install_completions_command.rs @@ -553,10 +553,11 @@ impl InstallCompletionsCommand { // Check if they need to load the zsh completions file into their .zshrc if shell == Shell::Zsh { - let mut completions_absolute_path_buf = PathBuffer::uninit(); - let completions_path = - bun_sys::get_fd_path(output_file.handle, &mut completions_absolute_path_buf) - .expect("unreachable"); + let mut completions_path_buf = PathBuffer::uninit(); + let completions_path: &[u8] = resolve_path::join_string_buf::( + &mut completions_path_buf, + &[completions_dir, filename], + ); let mut zshrc_filepath = PathBuffer::uninit(); let needs_to_tell_them_to_add_completions_file: bool = 'brk: { let dot_zshrc: File = 'zshrc: { @@ -693,8 +694,8 @@ impl InstallCompletionsCommand { if needs_to_tell_them_to_add_completions_file { pretty_errorln!( "To enable completions, add this to your .zshrc:\n [ -s \"{}\" ] && source \"{}\"", - bstr::BStr::new(&*completions_path), - bstr::BStr::new(&*completions_path), + bstr::BStr::new(completions_path), + bstr::BStr::new(completions_path), ); } } diff --git a/src/runtime/webcore/blob/read_file.rs b/src/runtime/webcore/blob/read_file.rs index fefb816d9ecd..384e91115a61 100644 --- a/src/runtime/webcore/blob/read_file.rs +++ b/src/runtime/webcore/blob/read_file.rs @@ -396,11 +396,10 @@ impl ReadFile { node: Default::default(), callback: Self::do_read_loop_task, }; - // On macOS, we use one-shot mode, so: + // On kqueue platforms we use one-shot mode, so: // - we don't need to unregister // - we don't need to delete from kqueue - #[cfg(target_os = "macos")] - { + if bun_core::Environment::IS_KQUEUE { // unless pending IO has been scheduled in-between. self.close_after_io = self.io_request.scheduled; } @@ -416,11 +415,10 @@ impl ReadFile { node: Default::default(), callback: Self::do_read_loop_task, }; - // On macOS, we use one-shot mode, so: + // On kqueue platforms we use one-shot mode, so: // - we don't need to unregister // - we don't need to delete from kqueue - #[cfg(target_os = "macos")] - { + if bun_core::Environment::IS_KQUEUE { // unless pending IO has been scheduled in-between. self.close_after_io = self.io_request.scheduled; } diff --git a/src/runtime/webcore/blob/write_file.rs b/src/runtime/webcore/blob/write_file.rs index 293d064020a5..42f384894d8a 100644 --- a/src/runtime/webcore/blob/write_file.rs +++ b/src/runtime/webcore/blob/write_file.rs @@ -476,9 +476,8 @@ impl WriteFile { // SAFETY: only reached via `WorkPoolTask::callback` with `task` = `&mut self.task` // (intrusive) registered in `on_writable`/`init`; recover parent. let this = unsafe { WriteFile::from_task_ptr(task) }; - // On macOS, we use one-shot mode, so we don't need to unregister. - #[cfg(target_os = "macos")] - { + // On kqueue platforms we use one-shot mode, so we don't need to unregister. + if bun_core::Environment::IS_KQUEUE { // SAFETY: `this` is the live parent (see above); scoped access. unsafe { (*this).close_after_io = false }; } diff --git a/src/spawn_sys/lib.rs b/src/spawn_sys/lib.rs index 397ec3a59584..ef83467c37d3 100644 --- a/src/spawn_sys/lib.rs +++ b/src/spawn_sys/lib.rs @@ -121,8 +121,12 @@ pub mod waiter_thread_flag { static SHOULD_USE_WAITER_THREAD: AtomicBool = AtomicBool::new(false); + /// The waiter thread is the fallback for Linux without pidfd. kqueue + /// platforms always have EVFILT_PROC, and the thread's loop has no wakeup + /// for newly appended processes there, so the flag is not honoured on them. #[inline] pub fn set() { + #[cfg(any(target_os = "linux", target_os = "android"))] SHOULD_USE_WAITER_THREAD.store(true, Ordering::Relaxed); } diff --git a/src/standalone_graph/StandaloneModuleGraph.rs b/src/standalone_graph/StandaloneModuleGraph.rs index f3e4e058152f..29225e0787b8 100644 --- a/src/standalone_graph/StandaloneModuleGraph.rs +++ b/src/standalone_graph/StandaloneModuleGraph.rs @@ -1174,12 +1174,50 @@ impl CompileResult { } } -pub(crate) fn inject( +/// The temp copy of the executable that `inject` wrote the module graph into: +/// its open fd plus the absolute path it was created at, which the caller +/// renames into place (an fd cannot be mapped back to a path on every +/// filesystem). +pub(crate) struct Injected<'a> { + pub fd: Fd, + pub temp_path: &'a ZStr, +} + +impl<'a> Injected<'a> { + /// `zname` was opened relative to `cwd` (or is already absolute); pin it in + /// `temp_path_buf` so a later `chdir` cannot retarget the rename/unlink. + fn new(fd: Fd, cwd: &[u8], zname: &ZStr, temp_path_buf: &'a mut PathBuffer) -> Injected<'a> { + let len = path::resolve_path::join_abs_string_buf_z::( + cwd, + &mut temp_path_buf[..], + &[zname.as_bytes()], + ) + .len(); + Injected { + fd, + temp_path: ZStr::from_buf(&temp_path_buf[..], len), + } + } +} + +pub(crate) fn inject<'a>( bytes: &[u8], self_exe: &ZStr, inject_options: &InjectOptions, target: &CompileTarget, -) -> Fd { + temp_path_buf: &'a mut PathBuffer, +) -> Option> { + let mut cwd_buf = bun_paths::path_buffer_pool::get(); + let cwd: &[u8] = match bun_sys::getcwd(&mut cwd_buf) { + Ok(len) => &cwd_buf[..len], + Err(err) => { + bun_core::pretty_errorln!( + "error: failed to get the current directory\n{}", + err + ); + return None; + } + }; let mut buf = PathBuffer::uninit(); // Note: `tmpname` borrows `buf` mutably for the &ZStr it returns. The // tmpdir-fallback retry below may need to repoint `zname` at a heap-owned @@ -1201,7 +1239,7 @@ pub(crate) fn inject( "error: failed to get temporary file name: {}", bstr::BStr::new(e.name()) ); - return Fd::INVALID; + return None; } }; @@ -1241,7 +1279,7 @@ pub(crate) fn inject( e.to_system_errno() .unwrap_or(bun_sys::SystemErrno::EUNKNOWN) ); - return Fd::invalid(); + return None; } let out = &out_buf[..zname.len()]; let file = match Syscall::open_file_at_windows( @@ -1260,7 +1298,7 @@ pub(crate) fn inject( "error: failed to open temporary file to copy bun into\n{}", e ); - return Fd::invalid(); + return None; } }; @@ -1334,9 +1372,14 @@ pub(crate) fn inject( bun_sys::E::EPERM | bun_sys::E::EAGAIN | bun_sys::E::EBUSY => { continue; } - _ => break, + _ => {} } } + bun_core::pretty_errorln!( + "error: failed to open temporary file to copy bun into\n{}", + err + ); + return None; } } } @@ -1363,7 +1406,7 @@ pub(crate) fn inject( err ); cleanup(zname, fd); - return Fd::INVALID; + return None; } } } @@ -1381,7 +1424,7 @@ pub(crate) fn inject( e ); cleanup(zname, fd); - return Fd::INVALID; + return None; } break 'brk fd; @@ -1396,7 +1439,7 @@ pub(crate) fn inject( Err(err) => { bun_core::pretty_errorln!("Error reading standalone module graph: {}", err); cleanup(zname, cloned_executable_fd); - return Fd::INVALID; + return None; } }; let mut macho_file = match bun_macho::MachoFile::init(&input_bytes, bytes.len()) { @@ -1404,20 +1447,20 @@ pub(crate) fn inject( Err(e) => { bun_core::pretty_errorln!("Error initializing standalone module graph: {}", e); cleanup(zname, cloned_executable_fd); - return Fd::INVALID; + return None; } }; if let Err(e) = macho_file.write_section(bytes) { bun_core::pretty_errorln!("Error writing standalone module graph: {}", e); cleanup(zname, cloned_executable_fd); - return Fd::INVALID; + return None; } drop(input_bytes); if let Err(err) = Syscall::set_file_offset(cloned_executable_fd, 0) { bun_core::pretty_errorln!("Error seeking to start of temporary file: {}", err); cleanup(zname, cloned_executable_fd); - return Fd::INVALID; + return None; } let mut buffered_writer = std::io::BufWriter::with_capacity( @@ -1430,19 +1473,24 @@ pub(crate) fn inject( bstr::BStr::new(e.name()) ); cleanup(zname, cloned_executable_fd); - return Fd::INVALID; + return None; } if let Err(e) = std::io::Write::flush(&mut buffered_writer) { bun_core::pretty_errorln!("Error flushing standalone module graph: {}", e); cleanup(zname, cloned_executable_fd); - return Fd::INVALID; + return None; } #[cfg(not(windows))] { // SAFETY: libc fchmod on a valid native fd. unsafe { bun_sys::c::fchmod(cloned_executable_fd.native(), 0o755) }; } - return cloned_executable_fd; + return Some(Injected::new( + cloned_executable_fd, + cwd, + zname, + temp_path_buf, + )); } CompileTargetOs::Windows => { let input_bytes = match bun_sys::File::borrow(&cloned_executable_fd).read_to_end() { @@ -1450,7 +1498,7 @@ pub(crate) fn inject( Err(err) => { bun_core::pretty_errorln!("Error reading standalone module graph: {}", err); cleanup(zname, cloned_executable_fd); - return Fd::INVALID; + return None; } }; let mut pe_file = match bun_pe::PEFile::init(&input_bytes) { @@ -1458,35 +1506,35 @@ pub(crate) fn inject( Err(e) => { bun_core::pretty_errorln!("Error initializing PE file: {}", e); cleanup(zname, cloned_executable_fd); - return Fd::INVALID; + return None; } }; if inject_options.hide_console { if let Err(e) = pe_file.set_subsystem(bun_pe::IMAGE_SUBSYSTEM_WINDOWS_GUI) { bun_core::pretty_errorln!("Error setting PE subsystem: {}", e); cleanup(zname, cloned_executable_fd); - return Fd::INVALID; + return None; } } // Always strip authenticode when adding .bun section for --compile if let Err(e) = pe_file.add_bun_section(bytes) { bun_core::pretty_errorln!("Error adding Bun section to PE file: {}", e); cleanup(zname, cloned_executable_fd); - return Fd::INVALID; + return None; } drop(input_bytes); if let Err(err) = Syscall::set_file_offset(cloned_executable_fd, 0) { bun_core::pretty_errorln!("Error seeking to start of temporary file: {}", err); cleanup(zname, cloned_executable_fd); - return Fd::INVALID; + return None; } let mut writer = bun_sys::FileWriter(cloned_executable_fd); if let Err(e) = pe_file.write(&mut writer) { bun_core::pretty_errorln!("Error writing PE file: {}", bstr::BStr::new(e.name())); cleanup(zname, cloned_executable_fd); - return Fd::INVALID; + return None; } // Truncate to the in-memory PE size; Authenticode strip can make it shorter than the base. if let Err(err) = Syscall::ftruncate( @@ -1495,7 +1543,7 @@ pub(crate) fn inject( ) { bun_core::pretty_errorln!("Error truncating PE file: {}", err); cleanup(zname, cloned_executable_fd); - return Fd::INVALID; + return None; } // Set executable permissions when running on POSIX hosts, even for Windows targets #[cfg(not(windows))] @@ -1503,7 +1551,12 @@ pub(crate) fn inject( // SAFETY: libc fchmod on a valid native fd. unsafe { bun_sys::c::fchmod(cloned_executable_fd.native(), 0o755) }; } - return cloned_executable_fd; + return Some(Injected::new( + cloned_executable_fd, + cwd, + zname, + temp_path_buf, + )); } CompileTargetOs::Linux | CompileTargetOs::Freebsd => { // ELF section approach: find .bun section and expand it @@ -1512,7 +1565,7 @@ pub(crate) fn inject( Err(err) => { bun_core::pretty_errorln!("Error reading executable: {}", err); cleanup(zname, cloned_executable_fd); - return Fd::INVALID; + return None; } }; @@ -1521,7 +1574,7 @@ pub(crate) fn inject( Err(e) => { bun_core::pretty_errorln!("Error initializing ELF file: {}", e); cleanup(zname, cloned_executable_fd); - return Fd::INVALID; + return None; } }; @@ -1530,13 +1583,13 @@ pub(crate) fn inject( if let Err(e) = elf_file.write_bun_section(bytes) { bun_core::pretty_errorln!("Error writing .bun section to ELF: {}", e); cleanup(zname, cloned_executable_fd); - return Fd::INVALID; + return None; } if let Err(err) = Syscall::set_file_offset(cloned_executable_fd, 0) { bun_core::pretty_errorln!("Error seeking to start of temporary file: {}", err); cleanup(zname, cloned_executable_fd); - return Fd::INVALID; + return None; } // Write the modified ELF data back to the file @@ -1544,7 +1597,7 @@ pub(crate) fn inject( if let Err(err) = write_file.write_all(&elf_file.data) { bun_core::pretty_errorln!("Error writing ELF file: {}", err); cleanup(zname, cloned_executable_fd); - return Fd::INVALID; + return None; } // Truncate the file to the exact size of the modified ELF if let Err(err) = Syscall::ftruncate( @@ -1553,7 +1606,7 @@ pub(crate) fn inject( ) { bun_core::pretty_errorln!("Error truncating ELF file: {}", err); cleanup(zname, cloned_executable_fd); - return Fd::INVALID; + return None; } #[cfg(not(windows))] @@ -1561,7 +1614,12 @@ pub(crate) fn inject( // SAFETY: libc fchmod on a valid native fd. unsafe { bun_sys::c::fchmod(cloned_executable_fd.native(), 0o755) }; } - return cloned_executable_fd; + return Some(Injected::new( + cloned_executable_fd, + cwd, + zname, + temp_path_buf, + )); } _ => { let total_byte_count: usize; @@ -1577,7 +1635,7 @@ pub(crate) fn inject( e ); cleanup(zname, cloned_executable_fd); - return Fd::invalid(); + return None; } }; } @@ -1589,7 +1647,7 @@ pub(crate) fn inject( Err(err) => { bun_core::pretty_errorln!("{}", err); cleanup(zname, cloned_executable_fd); - return Fd::INVALID; + return None; } }; break 'brk fstat.st_size.max(0); @@ -1613,7 +1671,7 @@ pub(crate) fn inject( seek_position ); cleanup(zname, cloned_executable_fd); - return Fd::INVALID; + return None; } } @@ -1627,7 +1685,7 @@ pub(crate) fn inject( err ); cleanup(zname, cloned_executable_fd); - return Fd::INVALID; + return None; } } } @@ -1640,7 +1698,12 @@ pub(crate) fn inject( unsafe { bun_sys::c::fchmod(cloned_executable_fd.native(), 0o755) }; } - return cloned_executable_fd; + return Some(Injected::new( + cloned_executable_fd, + cwd, + zname, + temp_path_buf, + )); } } } @@ -1903,11 +1966,23 @@ pub fn to_executable( bun_core::ZBox::from_vec_with_nul(dest_z.as_bytes().to_vec()) }; - let fd = inject(&bytes, &self_exe, windows_options, target); - // Note: a scopeguard closure capturing `fd` by value would not observe - // later reassignments; capturing by `&mut` conflicts with later uses. Explicit - // `if fd != Fd::INVALID { fd.close(); }` calls are inserted at every return below - // (both error and success paths). + let mut temp_path_buf = bun_paths::path_buffer_pool::get(); + let Some(injected) = inject( + &bytes, + &self_exe, + windows_options, + target, + &mut temp_path_buf, + ) else { + // inject() has already printed the specific error. + return Ok(CompileResult::fail_fmt(format_args!( + "failed to write compiled executable {}", + bstr::BStr::new(outfile) + ))); + }; + let fd = injected.fd; + // Closed explicitly at every return below rather than by a guard: on Windows + // the handle has to be closed mid-function, before `MoveFileExW`. debug_assert!(fd.kind() == bun_sys::FdKind::System); #[cfg(unix)] @@ -1918,20 +1993,7 @@ pub fn to_executable( #[cfg(windows)] { - // Get the current path of the temp file - let mut temp_buf = PathBuffer::uninit(); - let temp_path = match bun_sys::get_fd_path(fd, &mut temp_buf) { - Ok(p) => p, - Err(e) => { - if fd != Fd::INVALID { - fd.close(); - } - return Ok(CompileResult::fail_fmt(format_args!( - "Failed to get temp file path: {}", - bstr::BStr::new(e.name()) - ))); - } - }; + let temp_path: &[u8] = injected.temp_path.as_bytes(); // Build the absolute destination path // On Windows, we need an absolute path for MoveFileExW @@ -1940,9 +2002,7 @@ pub fn to_executable( let cwd_path: &[u8] = match bun_sys::getcwd(&mut cwd_buf) { Ok(len) => &cwd_buf[..len], Err(e) => { - if fd != Fd::INVALID { - fd.close(); - } + fd.close(); return Ok(CompileResult::fail_fmt(format_args!( "Failed to get current directory: {}", bstr::BStr::new(e.name()) @@ -1988,6 +2048,7 @@ pub fn to_executable( } == windows::FALSE { let werr = windows::Win32Error::get(); + let _ = Syscall::unlink(injected.temp_path); if let Some(sys_err) = werr.to_system_errno() { if sys_err == bun_sys::SystemErrno::EISDIR { return Ok(CompileResult::fail_fmt(format_args!( @@ -2040,24 +2101,7 @@ pub fn to_executable( #[cfg(not(windows))] { - let mut buf2 = PathBuffer::uninit(); - // Note: borrowck — `get_fd_path` returns `&mut [u8]` borrowing `buf2`; - // copy it into an owned buffer so `temp_posix_buf` can also borrow `buf2`'s - // sibling without overlap. - let temp_location: Vec = match bun_sys::get_fd_path(fd, &mut buf2) { - Ok(p) => p.to_vec(), - Err(e) => { - if fd != Fd::INVALID { - fd.close(); - } - return Ok(CompileResult::fail_fmt(format_args!( - "failed to get path for fd: {}", - e - ))); - } - }; - let mut temp_posix_buf = PathBuffer::uninit(); - let temp_posix = path::resolve_path::z(&temp_location, &mut temp_posix_buf); + let temp_posix = injected.temp_path; let outfile_basename = bun_paths::basename(outfile); let mut outfile_posix_buf = PathBuffer::uninit(); let outfile_posix = path::resolve_path::z(outfile_basename, &mut outfile_posix_buf); @@ -2077,16 +2121,14 @@ pub fn to_executable( } else { return Ok(CompileResult::fail_fmt(format_args!( "failed to rename {} to {}: {}", - bstr::BStr::new(&temp_location), + bstr::BStr::new(temp_posix.as_bytes()), bstr::BStr::new(outfile), bstr::BStr::new(e.name()) ))); } } - if fd != Fd::INVALID { - fd.close(); - } + fd.close(); Ok(CompileResult::Success) } } diff --git a/src/sys/lib.rs b/src/sys/lib.rs index c2e8b81781c7..b20e42e3c91b 100644 --- a/src/sys/lib.rs +++ b/src/sys/lib.rs @@ -7787,6 +7787,11 @@ pub fn get_fd_path<'a>(fd: Fd, out: &'a mut bun_paths::PathBuffer) -> Maybe<&'a // SAFETY: kernel wrote a NUL-terminated path into kf_path. let path_ptr = unsafe { addr_of!((*kif.as_ptr()).kf_path) } as *const u8; let len = unsafe { libc::strlen(path_ptr.cast()) }; + // The kernel fills kf_path from the namecache and leaves it empty when it + // has no name for the vnode (seen for a just-created file on UFS). + if len == 0 { + return Err(Error::from_code_int(libc::ENOENT, Tag::fcntl).with_fd(fd)); + } // SAFETY: path_ptr has `len` initialized bytes (kernel-written). out.0[..len].copy_from_slice(unsafe { core::slice::from_raw_parts(path_ptr, len) }); return Ok(&mut out.0[..len]); diff --git a/test/harness.ts b/test/harness.ts index a6c07623ec17..b3167919ec84 100644 --- a/test/harness.ts +++ b/test/harness.ts @@ -20,7 +20,9 @@ export const BREAKING_CHANGES_BUN_1_2 = false; export const isMacOS = process.platform === "darwin"; export const isLinux = process.platform === "linux"; export const isFreeBSD = process.platform === "freebsd"; -export const isPosix = isMacOS || isLinux || isFreeBSD; +/** Bun (like Node) reports `"android"` on Android; it is not folded into `isLinux`. */ +export const isAndroid = process.platform === "android"; +export const isPosix = isMacOS || isLinux || isFreeBSD || isAndroid; export const isWindows = process.platform === "win32"; export const isIntelMacOS = isMacOS && process.arch === "x64"; export const isArm64 = process.arch === "arm64"; @@ -1843,8 +1845,12 @@ export function libcPathForDlopen() { } case "darwin": return "libc.dylib"; + case "android": + return "libc.so"; + case "freebsd": + return "libc.so.7"; default: - throw new Error("TODO"); + throw new Error(`libcPathForDlopen: unsupported platform ${process.platform}`); } } diff --git a/test/js/bun/spawn/spawn.test.ts b/test/js/bun/spawn/spawn.test.ts index fd2a2c30c56c..327683631745 100644 --- a/test/js/bun/spawn/spawn.test.ts +++ b/test/js/bun/spawn/spawn.test.ts @@ -6,10 +6,10 @@ import { bunEnv, bunExe, getMaxFD, + isAndroid, isBroken, isDebug, isLinux, - isMacOS, isPosix, isWindows, shellExe, @@ -609,8 +609,10 @@ for (let [gcTick, label] of [ }); } -// This is a test which should only be used when pidfd and EVTFILT_PROC is NOT available -it.skipIf(Boolean(process.env.BUN_FEATURE_FLAG_FORCE_WAITER_THREAD) || !isPosix || isMacOS)( +// The waiter thread is the Linux fallback for kernels/sandboxes without pidfd; +// kqueue platforms (macOS, FreeBSD) always have EVFILT_PROC and its non-Linux +// loop has no wakeup for processes appended after it starts. +it.skipIf(Boolean(process.env.BUN_FEATURE_FLAG_FORCE_WAITER_THREAD) || (!isLinux && !isAndroid))( "with BUN_FEATURE_FLAG_FORCE_WAITER_THREAD", async () => { const result = spawnSync({ diff --git a/test/js/web/workers/worker-refused-completion.test.ts b/test/js/web/workers/worker-refused-completion.test.ts index 20936d3485ed..08d246187e35 100644 --- a/test/js/web/workers/worker-refused-completion.test.ts +++ b/test/js/web/workers/worker-refused-completion.test.ts @@ -10,7 +10,7 @@ // leaks. Builds with debug assertions only (debug, ASAN): the gate does not // exist in release builds. import { describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, isASAN, isDebug, isWindows } from "harness"; +import { bunEnv, bunExe, isAndroid, isASAN, isDebug, isLinux } from "harness"; type Row = { name: string; @@ -90,7 +90,8 @@ const ROWS: Row[] = [ refused: "ProcessWaiterThreadTask", // The waiter thread is a POSIX fallback path, opted into here the way the runtime's own tests do. env: { BUN_GARBAGE_COLLECTOR_LEVEL: "0", BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" }, - skip: isWindows, + // The flag is honoured on Linux/Android only (kqueue platforms always have EVFILT_PROC). + skip: !isLinux && !isAndroid, }, { name: "BroadcastChannel message from another thread",