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
33 changes: 19 additions & 14 deletions src/install/PackageInstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2121,7 +2121,11 @@ impl<'a> PackageInstall<'a> {
}
}
};
let dest = bun_paths::basename(dest_path.as_bytes());
// The entry name is the NUL-terminated tail of `dest_path`.
let dest: &ZStr = ZStr::from_slice_with_nul(
&dest_path.as_bytes_with_nul()[subdir.map_or(0, |dir| dir.len() + 1)..],
);
debug_assert_eq!(dest.as_bytes(), bun_paths::basename(dest_path.as_bytes()));
// When we're linking on Windows, we want to avoid keeping the source directory handle open
#[cfg(windows)]
{
Expand Down Expand Up @@ -2172,7 +2176,14 @@ impl<'a> PackageInstall<'a> {
dest_buf[offset] = bun_paths::SEP_WINDOWS;
offset += 1;
}
dest_buf[offset..offset + dest.len()].copy_from_slice(dest);
if offset + dest.len() >= dest_buf.len() {
return InstallResult::fail(
crate::Error::Sys(bun_errno::SystemErrno::ENAMETOOLONG),
Step::LinkingDependency,
None,
);
}
dest_buf[offset..offset + dest.len()].copy_from_slice(dest.as_bytes());
offset += dest.len();
dest_buf[offset] = 0;

Expand Down Expand Up @@ -2231,19 +2242,13 @@ impl<'a> PackageInstall<'a> {
Err(err) => return InstallResult::fail(err.into(), Step::LinkingDependency, None),
};

let target = path::resolve_path::relative(dest_dir_path, to_path);
// `symlinkat` takes `&ZStr` for both target and dest; build NUL-terminated
// copies in stack buffers.
let mut target_buf = PathBuffer::uninit();
target_buf[..target.len()].copy_from_slice(target);
target_buf[target.len()] = 0;
// SAFETY: NUL written above.
let target_z = ZStr::from_buf(&target_buf, target.len());
let mut dest_name_buf = [0u8; 512];
dest_name_buf[..dest.len()].copy_from_slice(dest);
// SAFETY: zero-initialized; NUL at [dest.len()].
let dest_z = ZStr::from_buf(&dest_name_buf, dest.len());
if let Err(err) = sys::symlinkat(target_z, dest_dir.fd(), dest_z) {
let target = path::resolve_path::relative_buf_z(
target_buf.as_mut_slice(),
dest_dir_path,
to_path,
);
if let Err(err) = sys::symlinkat(target, dest_dir.fd(), dest) {
return InstallResult::fail(err.into(), Step::LinkingDependency, None);
}
}
Expand Down
36 changes: 36 additions & 0 deletions test/cli/install/bun-workspaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
assertManifestsPopulated,
bunEnv as baseEnv,
bunExe,
isWindows,
readdirSorted,
runBunInstall,
toMatchNodeModulesAt,
Expand Down Expand Up @@ -2324,3 +2325,38 @@ describe("packages whose version label is longer than 512 bytes", () => {
);
});
});

// The hoisted installer links a workspace package into node_modules under its name. A name
// too long for the buffer that symlink is given used to abort the whole install instead of
// failing that one package with ENAMETOOLONG. On POSIX that buffer held 512 bytes. On
// Windows the name is appended to the absolute node_modules path in a 98302 byte buffer
// (bun refuses names of 98302 bytes and up), so the name has to come within a node_modules
// path (`\\?\C:\x\node_modules\` at the very least) of that size to overflow it.
describe("workspace packages whose name is too long to link", () => {
const longName = Buffer.alloc(isWindows ? 98302 - 20 : 600, "a").toString();

// A scoped package is linked inside a separately opened `node_modules/@scope` directory.
test.concurrent.each([
["unscoped", longName],
["scoped", `@scope/${longName}`],
])("%s name fails with ENAMETOOLONG", async (_, name) => {
using ctx = await setupTest();
const { packageDir, packageJson } = ctx;
await Promise.all([
write(packageJson, JSON.stringify({ name: "foo", workspaces: ["pkgs/*"] })),
write(join(packageDir, "pkgs", "pkg1", "package.json"), JSON.stringify({ name, version: "1.0.0" })),
]);

await using proc = spawn({
cmd: [bunExe(), "install", "--linker", "hoisted"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
env: ctx.env,
});
const [out, err, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(err).toContain(`ENAMETOOLONG: failed linking dependency/workspace to node_modules for package ${name}`);
expect(out).toContain("Failed to install 1 package");
expect(exitCode).toBe(1);
});
});
Loading