Skip to content
Open
20 changes: 15 additions & 5 deletions src/install/PackageInstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2306,6 +2306,12 @@ impl<'a> PackageInstall<'a> {
crate::PreinstallState::Done => false,
_ => 'brk: {
if self.patch.is_none() {
if !crate::package_manager_real::directories::cache_entry_is_dir(
self.cache_dir,
self.cache_dir_subpath,
) {
break 'brk true;
}
let exists = match resolution_tag {
resolution::Tag::Npm => 'package_json_exists: {
// SAFETY: `buf` and `self.cache_dir_subpath` both derive from the
Expand Down Expand Up @@ -2343,8 +2349,7 @@ impl<'a> PackageInstall<'a> {
ZStr::from_buf(&buf[..], subpath_len + 1 + b"package.json".len());
break 'package_json_exists sys::exists_at(self.cache_dir, subpath);
}
_ => sys::directory_exists_at(self.cache_dir, self.cache_dir_subpath)
.unwrap_or(false),
_ => true,
};
if exists {
manager.set_preinstall_state(package_id, crate::PreinstallState::Done);
Expand All @@ -2365,7 +2370,10 @@ impl<'a> PackageInstall<'a> {
// SAFETY: NUL written above.
let subpath =
ZStr::from_buf(&join_buf[..], cache_dir_subpath_without_patch_hash.len());
let exists = sys::directory_exists_at(self.cache_dir, subpath).unwrap_or(false);
let exists = crate::package_manager_real::directories::cache_entry_is_dir(
self.cache_dir,
subpath,
);
if exists {
manager.set_preinstall_state(package_id, crate::PreinstallState::Done);
}
Expand All @@ -2379,8 +2387,10 @@ impl<'a> PackageInstall<'a> {
manager: &mut PackageManager,
package_id: PackageID,
) -> bool {
let exists =
sys::directory_exists_at(self.cache_dir, self.cache_dir_subpath).unwrap_or(false);
let exists = crate::package_manager_real::directories::cache_entry_is_dir(
self.cache_dir,
self.cache_dir_subpath,
);
if exists {
manager.set_preinstall_state(package_id, crate::PreinstallState::Done);
}
Expand Down
23 changes: 18 additions & 5 deletions src/install/PackageInstaller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1492,15 +1492,28 @@ impl<'a> PackageInstaller<'a> {
self.summary.skipped += (!needs_install) as u32;

if needs_install {
// `--force` re-fetches on the first pass only; the post-download
// re-entry has NEEDS_VERIFY=false and links the fresh entry.
Comment thread
robobun marked this conversation as resolved.
let force_cache_refetch = NEEDS_VERIFY
&& self.force_install
&& self.manager().get_preinstall_state(package_id) != crate::PreinstallState::Done;
if resolution.tag.can_enqueue_install_task()
&& installer.package_missing_from_cache(
self.manager_mut(),
package_id,
resolution.tag,
)
&& (force_cache_refetch
|| installer.package_missing_from_cache(
self.manager_mut(),
package_id,
resolution.tag,
))
Comment thread
robobun marked this conversation as resolved.
{
debug_assert!(resolution.can_enqueue_install_task());

// Drop the derived `_patch_hash=` entry so the re-entry
// enqueues `ApplyPatch` against the fresh base.
Comment thread
robobun marked this conversation as resolved.
if force_cache_refetch && installer.patch.is_some() {
let _ = bun_sys::Dir::borrow(&installer.cache_dir)
.delete_tree(installer.cache_dir_subpath.as_bytes());
}
Comment thread
robobun marked this conversation as resolved.

let context =
TaskCallbackContext::DependencyInstallContext(DependencyInstallContext {
tree_id: self.current_tree_id,
Expand Down
56 changes: 54 additions & 2 deletions src/install/PackageManager/PackageManagerDirectories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,22 @@ unsafe fn ensure_cache_directory(this: *mut PackageManager) -> Dir {
unsafe { (*this).cache_directory_path = ZBox::from_bytes(&cache_dir.path) };

match Dir::cwd().make_open_path(&cache_dir.path, Default::default()) {
Ok(d) => return d,
Ok(d) => {
if is_trusted_cache_root(d.fd()) {
return d;
}
bun_core::pretty_errorln!(
"<r><yellow>warn<r>: ignoring install cache at <b>{}<r> because it is not a directory owned by the current user or is writable by other users. Set $BUN_INSTALL_CACHE_DIR to a directory only you can write to, or remove it.",
bun_fmt::s(&cache_dir.path)
);
Comment thread
robobun marked this conversation as resolved.
drop(d);
// SAFETY: narrow `&mut enable` projection; disjoint from
// any `&options.{registries,scope}` the caller may hold.
unsafe { (*this).options.enable.set(Enable::CACHE, false) };
// SAFETY: see fn safety contract.
unsafe { (*this).cache_directory_path = ZBox::from_bytes(b"") };
continue;
}
Err(_) => {
// SAFETY: narrow `&mut enable` projection; disjoint from
// any `&options.{registries,scope}` the caller may hold.
Expand Down Expand Up @@ -361,6 +376,22 @@ unsafe fn ensure_cache_directory(this: *mut PackageManager) -> Dir {
}
}

/// Cache hits never re-verify integrity, so refuse a shared cache root that
/// another user can write to; the caller falls back to `node_modules/.cache`.
Comment thread
robobun marked this conversation as resolved.
#[cfg(unix)]
fn is_trusted_cache_root(dir: Fd) -> bool {
match sys::fstat(dir) {
Ok(st) => sys::stat_is_owner_only_writable_dir(&st, bun_sys::c::getuid()),
Err(_) => false,
}
}
Comment thread
robobun marked this conversation as resolved.

#[cfg(not(unix))]
#[inline(always)]
fn is_trusted_cache_root(_dir: Fd) -> bool {
true
}
Comment thread
robobun marked this conversation as resolved.

pub struct CacheDir {
pub path: Vec<u8>,
pub is_node_modules: bool,
Expand Down Expand Up @@ -747,8 +778,29 @@ pub fn cached_tarball_folder_name(
)
}

/// `true` iff `subpath` under `dir` is a real directory (not a symlink or
/// junction), so a link planted at the predictable cache-entry name is treated
/// as absent and re-fetched. Windows `lstatat` maps junctions to `S_IFDIR`,
/// hence the explicit `FILE_ATTRIBUTE_REPARSE_POINT` query there.
Comment thread
robobun marked this conversation as resolved.
pub fn cache_entry_is_dir(dir: Fd, subpath: &ZStr) -> bool {
#[cfg(windows)]
{
match sys::get_file_attributes_at(dir, subpath) {
Some(a) => a.is_directory && !a.is_reparse_point,
None => false,
}
}
#[cfg(not(windows))]
{
match sys::lstatat(dir, subpath) {
Ok(st) => bun_sys::S::ISDIR(st.st_mode as _),
Err(_) => false,
}
}
}
Comment thread
robobun marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.

pub fn is_folder_in_cache(this: &mut PackageManager, folder_path: &ZStr) -> bool {
sys::directory_exists_at(get_cache_directory(this), folder_path).unwrap_or(false)
cache_entry_is_dir(get_cache_directory(this), folder_path)
}

// ─────────────────────────── global directories ───────────────────────────────
Expand Down
8 changes: 6 additions & 2 deletions src/install/PackageManager/PackageManagerLifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,11 @@ impl PackageManager {
return PreinstallState::Extract;
}

if directories::is_folder_in_cache(self, folder_path) {
// The cache is keyed only on name@version; `--force` must
// re-fetch and re-verify instead of trusting a hit.
Comment thread
robobun marked this conversation as resolved.
let trust_cache_hit = !self.options.enable.force_install();

if trust_cache_hit && directories::is_folder_in_cache(self, folder_path) {
self.set_preinstall_state(pkg.meta.id, PreinstallState::Done);
return PreinstallState::Done;
}
Expand All @@ -208,7 +212,7 @@ impl PackageManager {
});
// Owned NUL-terminated copy.
let non_patched_path = ZBox::from_bytes(&folder_path.as_bytes()[..idx]);
if directories::is_folder_in_cache(self, &non_patched_path) {
if trust_cache_hit && directories::is_folder_in_cache(self, &non_patched_path) {
self.set_preinstall_state(pkg.meta.id, PreinstallState::ApplyPatch);
// yay step 1 is already done for us
return PreinstallState::ApplyPatch;
Expand Down
4 changes: 3 additions & 1 deletion src/install/extract_tarball.rs
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,9 @@ impl ExtractTarball {
sys::Errno::NOTEMPTY
| sys::Errno::PERM
| sys::Errno::BUSY
| sys::Errno::EXIST => {
| sys::Errno::EXIST
// Junction/symlink at the destination → ENOTDIR.
| sys::Errno::NOTDIR => {
// before we attempt to delete the destination, let's close the source dir.
let _ = sys::close(dir_to_move);

Expand Down
45 changes: 28 additions & 17 deletions src/install/isolated_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2345,27 +2345,38 @@ pub(crate) fn install_isolated_packages(
let missing_from_cache = match installer.manager().get_preinstall_state(pkg_id)
{
install::PreinstallState::Done => false,
_ if installer.manager().options.enable.force_install() => {
// Drop the derived `_patch_hash=` entry so
// `apply_package_patch` re-derives from the fresh base.
Comment thread
robobun marked this conversation as resolved.
if matches!(patch_info, installer::PatchInfo::Patch(_)) {
let _ = bun_sys::Dir::borrow(&cache_dir)
.delete_tree(pkg_cache_dir_subpath.slice_z().as_bytes());
}
Comment thread
robobun marked this conversation as resolved.
true
}
_ => 'missing_from_cache: {
if matches!(patch_info, installer::PatchInfo::None) {
let exists = match pkg_res_tag {
ResolutionTag::Npm => {
// Reshaped for borrowck — capture length
// instead of `save()` so the path stays unborrowed.
let cache_dir_path_save = pkg_cache_dir_subpath.len();
pkg_cache_dir_subpath.append(b"package.json").assume_ok();
let exists = sys::exists_at(
cache_dir,
pkg_cache_dir_subpath.slice_z(),
);
pkg_cache_dir_subpath.set_length(cache_dir_path_save);
exists
}
_ => sys::directory_exists_at(
let exists =
crate::package_manager_real::directories::cache_entry_is_dir(
cache_dir,
pkg_cache_dir_subpath.slice_z(),
)
.unwrap_or(false),
};
) && match pkg_res_tag {
ResolutionTag::Npm => {
// Reshaped for borrowck — capture length
// instead of `save()` so the path stays unborrowed.
Comment thread
robobun marked this conversation as resolved.
let cache_dir_path_save = pkg_cache_dir_subpath.len();
pkg_cache_dir_subpath
.append(b"package.json")
.assume_ok();
let exists = sys::exists_at(
cache_dir,
pkg_cache_dir_subpath.slice_z(),
);
pkg_cache_dir_subpath.set_length(cache_dir_path_save);
exists
}
_ => true,
};
if exists {
installer.manager_mut().set_preinstall_state(
pkg_id,
Expand Down
7 changes: 4 additions & 3 deletions src/install/isolated_install/Installer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,9 +307,10 @@ impl<'a> Installer<'a> {
// contents hash, not the peer set). Once it exists, reuse it: rebuilding
// it replaces the directory under earlier entries' running hardlink tasks.
if let crate::patch_install::Callback::Apply(apply) = &patch_task.callback {
if sys::directory_exists_at(apply.cache_dir, apply.cache_dir_subpath.as_zstr())
.unwrap_or(false)
{
if crate::package_manager_real::directories::cache_entry_is_dir(
apply.cache_dir,
apply.cache_dir_subpath.as_zstr(),
) {
return;
}
}
Expand Down
5 changes: 1 addition & 4 deletions src/install/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -606,10 +606,7 @@ impl RunCommand {
Ok(()) => {}
Err(e) if e.get_errno() == bun_sys::E::EEXIST => match bun_sys::lstat(DIR_Z) {
Ok(st)
if bun_sys::kind_from_mode(st.st_mode as bun_sys::Mode)
== bun_sys::FileKind::Directory
&& st.st_uid == bun_sys::c::getuid()
&& (st.st_mode as bun_sys::Mode) & 0o022 == 0 => {}
if bun_sys::stat_is_owner_only_writable_dir(&st, bun_sys::c::getuid()) => {}
_ => return Ok(()),
},
Err(_) => return Ok(()),
Expand Down
6 changes: 1 addition & 5 deletions src/runtime/cli/bunx_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -569,11 +569,7 @@ impl BunxCommand {
#[cfg(unix)]
fn is_trusted_cache_root(cache_root: &ZStr, uid: libc::uid_t) -> bool {
match bun_sys::lstat(cache_root) {
Ok(st) => {
(st.st_mode & libc::S_IFMT) == libc::S_IFDIR
&& st.st_uid == uid
&& (st.st_mode & (libc::S_IWGRP | libc::S_IWOTH)) == 0
}
Ok(st) => bun_sys::stat_is_owner_only_writable_dir(&st, uid),
Err(_) => true,
}
}
Expand Down
Loading
Loading