Skip to content
9 changes: 3 additions & 6 deletions src/bun_core/feature_flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,9 @@ pub const DISABLE_AUTO_JS_TO_TS_IN_NODE_MODULES: bool = true;

pub const RUNTIME_TRANSPILER_CACHE: bool = true;

/// On Windows, node_modules/.bin uses pairs of '.exe' + '.bunx' files. The
/// fast path is to load the .bunx file within `bun.exe` instead of
/// `bun_shim_impl.exe` by using `bun_shim_impl.tryStartupFromBunJS`
///
/// When debugging weird script runner issues, it may be worth disabling this in
/// order to isolate your bug.
/// On Windows, the fast path reads the bin-shim metadata (`<exe>:bunx` stream
/// or `.bunx` sidecar) inside `bun.exe` via `bun_shim_impl::try_startup_from_bun_js`
/// instead of spawning `bun_shim_impl.exe`. Disable to isolate runner bugs.
Comment thread
robobun marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.
pub const WINDOWS_BUNX_FAST_PATH: bool = true;

// TODO: fix Windows-only test failures in fetch-preconnect.test.ts
Expand Down
138 changes: 85 additions & 53 deletions src/install/bin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1137,49 +1137,6 @@ impl<'a> Linker<'a> {
let abs_dest_w =
strings::convert_utf8_to_utf16_in_buffer(dest_buf.as_mut_slice(), abs_dest.as_bytes());
let abs_dest_w_len = abs_dest_w.len();
let bunx_suffix = w!(".bunx\x00");
dest_buf[abs_dest_w_len..abs_dest_w_len + bunx_suffix.len()].copy_from_slice(bunx_suffix);

// SAFETY: dest_buf[abs_dest_w_len + ".bunx".len()] == 0 written above
let abs_bunx_file =
bun_core::WStr::from_buf(&dest_buf[..], abs_dest_w_len + b".bunx".len());

let bunx_file = 'bunx_file: {
match sys::File::openat_os_path(
Fd::invalid(),
abs_bunx_file,
sys::O::WRONLY | sys::O::CREAT | sys::O::TRUNC,
0o664,
) {
Ok(f) => break 'bunx_file f,
Err(err) => {
let err: crate::Error = err.into();
if err != crate::Error::Sys(bun_errno::SystemErrno::ENOENT) || global {
self.err = Some(err);
return;
}

// Snapshot the length and restore via `set_length` after.
let node_modules_path_save = self.node_modules_path.len();
let _ = self.node_modules_path.append(b".bin");
let _ = sys::Dir::cwd().make_path(self.node_modules_path.slice());
self.node_modules_path.set_length(node_modules_path_save);

match sys::File::openat_os_path(
Fd::invalid(),
abs_bunx_file,
sys::O::WRONLY | sys::O::CREAT | sys::O::TRUNC,
0o664,
) {
Ok(f) => break 'bunx_file f,
Err(real_err) => {
self.err = Some(real_err.into());
return;
}
}
}
}
};

let rel_target = resolve_path::relative_buf_z(
self.rel_buf,
Expand Down Expand Up @@ -1237,28 +1194,103 @@ impl<'a> Linker<'a> {
return;
}

if let Err(err) = bunx_file.write_all(metadata) {
self.err = Some(err.into());
return;
}

// Exe first so the `:bunx` stream below has a host file.
let exe_suffix = w!(".exe\x00");
dest_buf[abs_dest_w_len..abs_dest_w_len + exe_suffix.len()].copy_from_slice(exe_suffix);
// SAFETY: dest_buf[abs_dest_w_len + ".exe".len()] == 0 written above
let abs_exe_file = bun_core::WStr::from_buf(&dest_buf[..], abs_dest_w_len + b".exe".len());

if let Err(err) = sys::File::write_file_os_path(
let exe_rewritten = match sys::File::write_file_os_path(
Fd::invalid(),
abs_exe_file,
crate::windows_shim::embedded_executable_data(),
) {
let err: crate::Error = err.into();
if err == crate::Error::Sys(bun_errno::SystemErrno::EBUSY) {
// exe is most likely running. bunx file has already been updated, ignore error
Ok(()) => true,
Err(err) => {
let err: crate::Error = err.into();
match err {
crate::Error::Sys(bun_errno::SystemErrno::EBUSY) => false,
crate::Error::Sys(bun_errno::SystemErrno::ENOENT) if !global => {
let node_modules_path_save = self.node_modules_path.len();
let _ = self.node_modules_path.append(b".bin");
let _ = sys::Dir::cwd().make_path(self.node_modules_path.slice());
self.node_modules_path.set_length(node_modules_path_save);

if let Err(real_err) = sys::File::write_file_os_path(
Fd::invalid(),
abs_exe_file,
crate::windows_shim::embedded_executable_data(),
) {
self.err = Some(real_err.into());
return;
}
true
}
_ => {
self.err = Some(err);
return;
}
}
}
};

// Prefer the `:bunx` alternate data stream; fall back to a `<name>.bunx`
// sidecar on any error (exFAT, some network shares lack named streams).
Comment thread
robobun marked this conversation as resolved.
let ads_suffix = w!(".exe:bunx\x00");
dest_buf[abs_dest_w_len..abs_dest_w_len + ads_suffix.len()].copy_from_slice(ads_suffix);
// SAFETY: dest_buf[abs_dest_w_len + ".exe:bunx".len()] == 0 written above
let abs_ads_file =
bun_core::WStr::from_buf(&dest_buf[..], abs_dest_w_len + b".exe:bunx".len());

let wrote_ads = match sys::File::openat_os_path(
Fd::invalid(),
abs_ads_file,
sys::O::WRONLY | sys::O::CREAT | sys::O::TRUNC,
0o664,
) {
Ok(ads_file) => {
if let Err(err) = ads_file.write_all(metadata) {
self.err = Some(err.into());
return;
}
true
}
Err(_) => {
// Readers probe the stream first, so a stale one from a prior
// install must not survive the sidecar fallback.
Comment thread
robobun marked this conversation as resolved.
let _ = sys::unlink_w(abs_ads_file);
false
}
};

let bunx_suffix = w!(".bunx\x00");
dest_buf[abs_dest_w_len..abs_dest_w_len + bunx_suffix.len()].copy_from_slice(bunx_suffix);
// SAFETY: dest_buf[abs_dest_w_len + ".bunx".len()] == 0 written above
let abs_bunx_file =
bun_core::WStr::from_buf(&dest_buf[..], abs_dest_w_len + b".bunx".len());

if wrote_ads && exe_rewritten {
let _ = sys::unlink_w(abs_bunx_file);
return;
}

// Sidecar write: either the fallback, or (when the exe was EBUSY) a
// fresh copy so a pre-ADS shim PE left on disk still finds metadata.
Comment thread
robobun marked this conversation as resolved.
let bunx_file = match sys::File::openat_os_path(
Fd::invalid(),
abs_bunx_file,
sys::O::WRONLY | sys::O::CREAT | sys::O::TRUNC,
0o664,
) {
Ok(f) => f,
Err(err) => {
self.err = Some(err.into());
return;
}
};

self.err = Some(err);
if let Err(err) = bunx_file.write_all(metadata) {
self.err = Some(err.into());
return;
}
Comment thread
robobun marked this conversation as resolved.
}
Comment thread
claude[bot] marked this conversation as resolved.
Expand Down
172 changes: 109 additions & 63 deletions src/install/windows-shim/bun_shim_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
//! This also solves the 'Terminate batch job (Y/N)' problem you see when using NPM/Yarn,
//! which is a HUGE dx win for developers.
//!
//! The approach implemented is a `.bunx` file which sits right next to the renamed
//! launcher exe. We read that (see `BinLinkingShim.rs` for the creation of this file)
//! and then we call NtCreateProcess to spawn the correct child process.
//! The metadata (target path + parsed shebang; encoded by `BinLinkingShim.rs`)
//! lives in a `:bunx` NTFS stream on the launcher exe, or a sibling `.bunx`
//! file when the volume lacks named streams. We probe the stream first, fall
//! back to the sidecar, then NtCreateProcess the decoded target.
Comment thread
robobun marked this conversation as resolved.
//!
//! Every attempt possible to make this file as minimal as possible has been made.
//! Which has unfortunatly made is difficult to read. To make up for this, every
Expand Down Expand Up @@ -585,8 +586,16 @@ fn launcher<const MODE: LauncherMode, Ctx: BunCtx>(bun_ctx: Ctx) -> LauncherRet
);
}
}
let image_path_to_copy_b_len = image_path_b_len - 2 * suffix.len();
// SAFETY: buf1 has room for nt_prefix + image_path; image_path_u8 is valid for the copy len.
// Standalone keeps `.exe` so `:bunx` can be appended for the stream probe.
let image_path_to_copy_b_len = if IS_STANDALONE {
image_path_b_len
} else {
image_path_b_len - 2 * suffix.len()
};
if NT_OBJECT_PREFIX.len() + image_path_to_copy_b_len / 2 > BUF1_LEN {
return LauncherMode::fail(MODE, FailReason::InvalidShimBounds);
}
// SAFETY: bounds checked above; image_path_u8 is valid for the copy len.
unsafe {
core::ptr::copy_nonoverlapping(
image_path_u8.as_ptr(),
Expand All @@ -599,68 +608,105 @@ fn launcher<const MODE: LauncherMode, Ctx: BunCtx>(bun_ctx: Ctx) -> LauncherRet
let mut metadata_handle: HANDLE = core::ptr::null_mut();
let mut io: IO_STATUS_BLOCK = bun_core::ffi::zeroed();
if IS_STANDALONE {
// BUF1: '\??\C:\Users\chloe\project\node_modules\.bin\hello.bunx!!!!!!!!!!!!!!!!!!!!!!'
// SAFETY: writing 4 u16s ("bunx") into buf1 at the computed offset, which is in bounds.
unsafe {
buf1_u8
.add(image_path_b_len + 2 * (NT_OBJECT_PREFIX.len() - 3/* "exe".len */))
.cast::<[u16; 4]>()
.write_unaligned(['b' as u16, 'u' as u16, 'n' as u16, 'x' as u16]);
}
let open_metadata = |path_len_bytes: u16,
out_handle: &mut HANDLE,
io: &mut IO_STATUS_BLOCK|
-> nt::Status {
let mut nt_name = UNICODE_STRING {
Length: path_len_bytes,
MaximumLength: path_len_bytes,
Buffer: buf1_u16,
};
if DBG {
debug!(
"NtCreateFile({})",
fmt16(unsafe { unicode_string_to_u16(&nt_name) })
);
// NtCreateFile requires the `\??\` object prefix for absolute paths.
debug_assert!(
unsafe { unicode_string_to_u16(&nt_name) }.starts_with(&NT_OBJECT_PREFIX)
);
debug_assert!(
unsafe { unicode_string_to_u16(&nt_name) }.ends_with(bun_core::w!("bunx"))
);
}
let mut attr = w::OBJECT_ATTRIBUTES {
Length: size_of::<w::OBJECT_ATTRIBUTES>() as u32,
RootDirectory: core::ptr::null_mut(),
Attributes: 0, // Note we do not use OBJ_CASE_INSENSITIVE here.
ObjectName: &mut nt_name,
SecurityDescriptor: core::ptr::null_mut(),
SecurityQualityOfService: core::ptr::null_mut(),
};
// SAFETY: all out-pointers are valid stack locations; attr is fully initialized.
unsafe {
nt::NtCreateFile(
out_handle,
FILE_GENERIC_READ,
&mut attr,
io,
core::ptr::null_mut(),
w::FILE_ATTRIBUTE_NORMAL,
w::FILE_SHARE_WRITE | w::FILE_SHARE_READ | w::FILE_SHARE_DELETE,
w::FILE_OPEN,
w::FILE_NON_DIRECTORY_FILE | w::FILE_SYNCHRONOUS_IO_NONALERT,
core::ptr::null_mut(),
0,
)
}
};

let path_len_bytes: u16 = u16::try_from(
image_path_b_len + 2 * (NT_OBJECT_PREFIX.len() - 3 /* "exe".len */ + 4/* "bunx".len */),
)
.unwrap();
let mut nt_name = UNICODE_STRING {
Length: path_len_bytes,
MaximumLength: path_len_bytes,
Buffer: buf1_u16,
// `UNICODE_STRING.Length` is u16, so guard on the byte length; the
// same bound covers buf1 (`u16::MAX/2 < BUF1_LEN`).
Comment thread
robobun marked this conversation as resolved.
let nt_len_bytes = |len_u16s: usize| -> Option<u16> {
if len_u16s <= BUF1_LEN {
u16::try_from(2 * len_u16s).ok()
} else {
None
}
};
if DBG {
debug!(
"NtCreateFile({})",
fmt16(unsafe { unicode_string_to_u16(&nt_name) })
);
debug!(
"NtCreateFile({})",
fmt16(unsafe { unicode_string_to_u16(&nt_name) })
);
}
let mut attr = w::OBJECT_ATTRIBUTES {
Length: size_of::<w::OBJECT_ATTRIBUTES>() as u32,
RootDirectory: core::ptr::null_mut(),
Attributes: 0, // Note we do not use OBJ_CASE_INSENSITIVE here.
ObjectName: &mut nt_name,
SecurityDescriptor: core::ptr::null_mut(),
SecurityQualityOfService: core::ptr::null_mut(),

let ads_path_len = NT_OBJECT_PREFIX.len() + image_path_b_len / 2 + 5 /* ":bunx".len */;
let mut rc = match nt_len_bytes(ads_path_len) {
Some(path_len_bytes) => {
// BUF1: '\??\C:\Users\chloe\project\node_modules\.bin\hello.exe:bunx!!!!!!!!!!!!!!!!!!'
// SAFETY: ads_path_len <= BUF1_LEN guarantees the 5 u16s after the copied image
// path are within buf1.
unsafe {
buf1_u8
.add(image_path_b_len + 2 * NT_OBJECT_PREFIX.len())
.cast::<[u16; 5]>()
.write_unaligned([
':' as u16, 'b' as u16, 'u' as u16, 'n' as u16, 'x' as u16,
]);
}
open_metadata(path_len_bytes, &mut metadata_handle, &mut io)
}
None => NTSTATUS::OBJECT_NAME_NOT_FOUND,
};
// NtCreateFile will fail for absolute paths if we do not pass an OBJECT name
// so we need the prefix here. This is an extra sanity check.
if DBG {
debug_assert!(
unsafe { unicode_string_to_u16(&nt_name) }.starts_with(&NT_OBJECT_PREFIX)
);
debug_assert!(
unsafe { unicode_string_to_u16(&nt_name) }.ends_with(bun_core::w!(".bunx"))
);

if rc != NTSTATUS::SUCCESS {
if DBG {
debug!("ADS open failed ({}), trying .bunx sidecar", rc.0);
}
let sidecar_path_len =
NT_OBJECT_PREFIX.len() + image_path_b_len / 2 - 3 /* "exe".len */ + 4 /* "bunx".len */;
let Some(path_len_bytes) = nt_len_bytes(sidecar_path_len) else {
return LauncherMode::fail(MODE, FailReason::InvalidShimBounds);
};
// BUF1: '\??\C:\Users\chloe\project\node_modules\.bin\hello.bunxbunx!!!!!!!!!!!!!!!!!!'
// ^^^^ overwritten, trailing
// bytes ignored by Length
// SAFETY: sidecar_path_len <= BUF1_LEN guarantees the 4 u16s are within buf1.
unsafe {
buf1_u8
.add(image_path_b_len + 2 * (NT_OBJECT_PREFIX.len() - 3/* "exe".len */))
.cast::<[u16; 4]>()
.write_unaligned(['b' as u16, 'u' as u16, 'n' as u16, 'x' as u16]);
}
rc = open_metadata(path_len_bytes, &mut metadata_handle, &mut io);
}
// SAFETY: all out-pointers are valid stack locations; attr is fully initialized.
let rc = unsafe {
nt::NtCreateFile(
&mut metadata_handle,
FILE_GENERIC_READ,
&mut attr,
&mut io,
core::ptr::null_mut(),
w::FILE_ATTRIBUTE_NORMAL,
w::FILE_SHARE_WRITE | w::FILE_SHARE_READ | w::FILE_SHARE_DELETE,
w::FILE_OPEN,
w::FILE_NON_DIRECTORY_FILE | w::FILE_SYNCHRONOUS_IO_NONALERT,
core::ptr::null_mut(),
0,
)
};

if rc != NTSTATUS::SUCCESS {
if DBG {
debug!("error opening: {}", rc.0);
Expand Down
Loading
Loading