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
6 changes: 6 additions & 0 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4875,6 +4875,12 @@ pub mod bv2_impl {
self.transpiler.log_mut(),
source.path.pretty,
&output_path,
match self.transpiler.options.compile_mode {
options::CompileMode::None => &output_path,
// An executable embeds its assets and a standalone HTML build inlines them.
options::CompileMode::Executable
| options::CompileMode::StandaloneHtml => b"",
},
Comment thread
robobun marked this conversation as resolved.
)
{
has_unusable_output_path = true;
Expand Down
19 changes: 13 additions & 6 deletions src/bundler/linker_context/computeChunks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -721,12 +721,19 @@ pub(crate) fn compute_chunks(
};
let mut real_path_buf = bun_paths::path_buffer_pool::get();
let dir: &[u8] = 'dir: {
let Ok(dir_file) = bun_sys::File::openat(
bun_sys::Fd::cwd(),
dir_path,
bun_sys::O::PATH | bun_sys::O::DIRECTORY,
0,
) else {
// With a NUL byte the open would read the path only up to it, and name another directory.
let dir_file = if strings::contains_char(dir_path, 0) {
None
} else {
bun_sys::File::openat(
bun_sys::Fd::cwd(),
dir_path,
bun_sys::O::PATH | bun_sys::O::DIRECTORY,
0,
)
.ok()
};
let Some(dir_file) = dir_file else {
let Some(normalized) = resolve_path::normalize_buf_checked::<
bun_paths::platform::Auto,
>(dir_path, &mut real_path_buf.0) else {
Expand Down
28 changes: 22 additions & 6 deletions src/bundler/linker_context/generateChunksInParallel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,13 +433,29 @@ pub(crate) fn generate_chunks_in_parallel<const IS_DEV_SERVER: bool>(
[chunk.entry_point.source_index() as usize]
.path
.pretty;
if opens_output_paths
&& !chunk
if opens_output_paths {
// Inlined and embedded chunks are not opened, but their `.map` files are: an executable's by basename.
let has_map = chunk
.content
.sourcemap(c.options.source_maps)
.has_external_files();
let opened: &[u8] = match c.options.compile_mode {
options::CompileMode::None => &rel_path,
options::CompileMode::StandaloneHtml
if has_map || matches!(chunk.content, crate::chunk::Content::Html) =>
{
&rel_path
}
options::CompileMode::Executable if has_map => path::basename(&rel_path),
options::CompileMode::StandaloneHtml | options::CompileMode::Executable => b"",
};
if !chunk
.template
.check_output_path(c.log_disjoint(), input, &rel_path)
{
has_unusable_output_path = true;
continue;
.check_output_path(c.log_disjoint(), input, &rel_path, opened)
{
has_unusable_output_path = true;
continue;
}
}

let claimed = path_names_map.get_or_put(&rel_path)?;
Expand Down
17 changes: 16 additions & 1 deletion src/bundler/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2495,13 +2495,28 @@ impl PathTemplate {
/// Max output path length. A `PathBuffer` also needs room for a `.map` or `.jsc` suffix and the NUL.
pub(crate) const MAX_OUTPUT_PATH_LEN: usize = bun_paths::MAX_PATH_BYTES - 1 - ".map".len();

/// Logs an error and returns `false` when `output_path` exceeds `MAX_OUTPUT_PATH_LEN`.
/// Logs an error and returns `false` if `output_path` is too long or `opened`, the part the build opens, has a NUL.
pub(crate) fn check_output_path(
&self,
log: &mut bun_ast::Log,
input: &[u8],
output_path: &[u8],
opened: &[u8],
) -> bool {
if strings::contains_char(opened, 0) {
log.add_range_error_fmt_with_note(
None,
bun_ast::Range::NONE,
format_args!(
"Output path for {} contains a null byte: {}",
bun_core::fmt::quote(input),
bun_core::fmt::quote(output_path),
),
format_args!("naming template is {}", bun_core::fmt::quote(&self.data)),
bun_ast::Range::NONE,
);
return false;
}
if output_path.len() <= Self::MAX_OUTPUT_PATH_LEN {
return true;
}
Expand Down
70 changes: 69 additions & 1 deletion src/runtime/api/JSBundler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,34 @@ use bun_bundler_jsc::options_jsc::{compile_target_from_js, compile_target_from_s

pub(crate) mod js_bundler {
use super::*;
use crate::node::types::Valid;
use bun_core::Utf8Bytes;

use bun_sys::FdExt;

type OwnedString = MutableString;

/// Throws if `value`, which comes from `entrypoints` unless `option` is set, has a NUL byte.
fn derived_no_null_bytes(
global: &JSGlobalObject,
what: &str,
value: &[u8],
option: &str,
) -> JsResult<()> {
if !bun_core::strings::contains_char(value, 0) {
return Ok(());
}
Err(global
.err(
jsc::ErrorCode::INVALID_ARG_VALUE,
format_args!(
"The {what} {}, taken from 'entrypoints', must not contain null bytes. Set '{option}' to override it.",
bun_core::fmt::quote(value)
),
)
.throw())
}

/// `options::JSX::Runtime` → `api::JsxRuntime` (only the reverse `From`
/// exists upstream).
fn jsx_runtime_to_api(r: options::JSX::Runtime) -> api::JsxRuntime {
Expand Down Expand Up @@ -341,6 +363,12 @@ pub(crate) mod js_bundler {
object.get_own(global_this, &BunString::static_("executablePath"))?
{
let slice = executable_path.to_utf8(global_this)?;
Valid::no_null_bytes(
slice.slice(),
"compile.executablePath",
"a string",
global_this,
)?;
let path_z = bun_core::ZBox::from_bytes(slice.slice());
if bun_sys::exists_at_type(bun_sys::Fd::cwd(), path_z.as_zstr())
.unwrap_or(bun_sys::ExistsAtType::Directory)
Expand Down Expand Up @@ -370,6 +398,12 @@ pub(crate) mod js_bundler {
windows.get_own(global_this, &BunString::static_("icon"))?
{
let slice = windows_icon_path.to_utf8(global_this)?;
Valid::no_null_bytes(
slice.slice(),
"compile.windows.icon",
"a string",
global_this,
)?;
let path_z = bun_core::ZBox::from_bytes(slice.slice());
if bun_sys::exists_at_type(bun_sys::Fd::cwd(), path_z.as_zstr())
.unwrap_or(bun_sys::ExistsAtType::Directory)
Expand Down Expand Up @@ -421,6 +455,7 @@ pub(crate) mod js_bundler {

if let Some(outfile) = object.get_own(global_this, &BunString::static_("outfile"))? {
let slice = outfile.to_utf8(global_this)?;
Valid::no_null_bytes(slice.slice(), "compile.outfile", "a string", global_this)?;
this.outfile.append_slice_exact(slice.slice())?;
}

Expand Down Expand Up @@ -475,6 +510,12 @@ pub(crate) mod js_bundler {
"compile.bytecodeOrder must not contain an empty path"
)));
}
Valid::no_null_bytes(
slice.slice(),
"compile.bytecodeOrder",
"a string",
global_this,
)?;
this.bytecode_order.push(Box::from(slice.slice()));
Ok(())
};
Expand Down Expand Up @@ -742,12 +783,14 @@ pub(crate) mod js_bundler {

let mut has_out_dir = false;
if let Some(slice) = config.get_optional_slice(global_this, b"outdir")? {
Valid::no_null_bytes(slice.slice(), "outdir", "a string", global_this)?;
this.outdir.append_slice_exact(slice.slice())?;
has_out_dir = true;
drop(slice);
}

if let Some(slice) = config.get_optional_slice(global_this, b"tsconfig")? {
Valid::no_null_bytes(slice.slice(), "tsconfig", "a string", global_this)?;
this.tsconfig_override.append_slice_exact(slice.slice())?;
drop(slice);
}
Expand Down Expand Up @@ -1004,8 +1047,11 @@ pub(crate) mod js_bundler {
}

{
let given = config.get_optional_slice(global_this, b"root")?;
let is_default = given.is_none();
let path: Utf8Bytes = 'brk: {
if let Some(slice) = config.get_optional_slice(global_this, b"root")? {
if let Some(slice) = given {
Valid::no_null_bytes(slice.slice(), "root", "a string", global_this)?;
break 'brk slice;
}

Expand Down Expand Up @@ -1041,6 +1087,9 @@ pub(crate) mod js_bundler {
.unwrap_or(b"."),
);
};
if is_default {
derived_no_null_bytes(global_this, "root directory", path.slice(), "root")?;
}

let dir = match bun_sys::open_dir_at(bun_sys::Fd::cwd(), path.slice()) {
Ok(d) => d,
Expand Down Expand Up @@ -1291,18 +1340,31 @@ pub(crate) mod js_bundler {
// metafile: "path/to/meta.json" - shorthand for { json: "..." }
this.metafile = true;
let slice = metafile_value.to_utf8(global_this)?;
Valid::no_null_bytes(slice.slice(), "metafile", "a string", global_this)?;
this.metafile_json_path.append_slice_exact(slice.slice())?;
drop(slice);
} else if metafile_value.is_object() {
// metafile: { json?: string, markdown?: string }
this.metafile = true;
if let Some(slice) = metafile_value.get_optional_slice(global_this, b"json")? {
Valid::no_null_bytes(
slice.slice(),
"metafile.json",
"a string",
global_this,
)?;
this.metafile_json_path.append_slice_exact(slice.slice())?;
drop(slice);
}
if let Some(slice) =
metafile_value.get_optional_slice(global_this, b"markdown")?
{
Valid::no_null_bytes(
slice.slice(),
"metafile.markdown",
"a string",
global_this,
)?;
this.metafile_markdown_path
.append_slice_exact(slice.slice())?;
drop(slice);
Expand Down Expand Up @@ -1384,6 +1446,12 @@ pub(crate) mod js_bundler {
return Err(global_this.throw_invalid_arguments(format_args!("cannot use compile with an output file named 'bun' because bun won't realize it's a standalone executable. Please choose a different name for compile.outfile")));
}

derived_no_null_bytes(
global_this,
"executable name",
outfile,
"compile.outfile",
)?;
compile.outfile.append_slice_exact(outfile)?;
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/runtime/cli/build_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,15 @@ impl BuildCommand {
resolve_path::get_if_exists_longest_common_path(&entries).unwrap_or(b".")
};

// An entry point from bunfig.toml can have a NUL byte, and the open would read the path only up to it.
if strings::contains_char(path, 0) {
bun_core::pretty_errorln!(
"<r><red>error<r><d>:<r> root directory {} contains a null byte",
bun_fmt::quote(path),
);
Global::exit(1);
}

// `bun_sys::Dir` is the owning RAII
// handle; its Drop closes the fd when this block ends.
let dir = match bun_sys::open_dir_at(Fd::cwd(), path) {
Expand Down
24 changes: 21 additions & 3 deletions src/runtime/node/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,8 @@ impl Valid {
Self::no_null_bytes(slice, "path", "a string, Uint8Array, or URL", global)
}

/// Inlined down to the scan: the `fs` functions run it on every path.
#[inline]
pub(crate) fn no_null_bytes(
slice: &[u8],
name: &str,
Expand All @@ -1254,15 +1256,31 @@ impl Valid {
if strings::index_of_char(slice, 0).is_none() {
return Ok(());
}
Err(global
Err(Self::throw_null_bytes(slice, name, accepts, global))
}

#[cold]
fn throw_null_bytes(
slice: &[u8],
name: &str,
accepts: &str,
global: &JSGlobalObject,
) -> jsc::JsError {
// Like Node's ERR_INVALID_ARG_VALUE: a dotted name is a property.
let kind = if strings::contains_char(name.as_bytes(), b'.') {
"property"
} else {
"argument"
};
global
.err(
jsc::ErrorCode::INVALID_ARG_VALUE,
format_args!(
"The argument '{name}' must be {accepts} without null bytes. Received {}",
"The {kind} '{name}' must be {accepts} without null bytes. Received {}",
bun_fmt::quote(slice)
),
)
.throw())
.throw()
}
}

Expand Down
Loading
Loading