Skip to content
Merged
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
26 changes: 13 additions & 13 deletions src/ast/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -631,14 +631,14 @@ pub struct Location {
// - 4-byte fields last: i32
// This eliminates padding between differently-sized fields.
//
// `file` / `line_text` are `Cow` (not `Str`) because
// `file` / `namespace` / `line_text` are `Cow` (not `Str`) because
// `Location::clone()` must deep-dupe them so a
// `BuildMessage`/`ResolveMessage` that outlives the
// `Source.contents` it borrowed from doesn't read poisoned memory. The
// `Source` it borrowed from doesn't read poisoned memory. The
// borrowed arm covers the common case where the slice points into
// arena-owned source text.
// the bundle's arena.
pub file: Cow<'static, [u8]>,
pub namespace: Str,
pub namespace: Cow<'static, [u8]>,
/// Text on the line, avoiding the need to refetch the source code
pub line_text: Option<Cow<'static, [u8]>>,
/// Number of bytes this location should highlight.
Expand All @@ -657,9 +657,9 @@ pub struct Location {
pub column: i32,
}

// NOT `#[derive(Clone)]`. `file` / `line_text` are
// NOT `#[derive(Clone)]`. `file` / `namespace` / `line_text` are
// `Cow<'static, [u8]>` whose `Borrowed` arm may carry a lifetime-erased view
// into `Source.contents` (see `init_or_null`, `css_parser.rs`, `error.rs`,
// into a `Source` (see `init_or_null`, `css_parser.rs`, `error.rs`,
// `JSBundler.rs`). The derived `Cow::clone` would re-borrow that pointer, so a
// `BuildMessage` cloned via `Option<Location>::clone()` / `Vec<Data>::clone()`
// could outlive the source buffer and read poisoned memory. Instead,
Expand All @@ -668,7 +668,7 @@ impl Clone for Location {
fn clone(&self) -> Self {
Location {
file: Cow::Owned(self.file.to_vec()),
namespace: self.namespace,
namespace: Cow::Owned(self.namespace.to_vec()),
line: self.line,
column: self.column,
length: self.length,
Expand All @@ -682,7 +682,7 @@ impl Default for Location {
fn default() -> Self {
Location {
file: Cow::Borrowed(b""),
namespace: b"file",
namespace: Cow::Borrowed(b"file"),
line_text: None,
length: 0,
offset: 0,
Expand All @@ -705,7 +705,7 @@ impl Location {

pub fn count(&self, builder: &mut StringBuilder) {
builder.count(self.file.as_ref().into_str());
builder.count(self.namespace);
builder.count(self.namespace.as_ref().into_str());
if let Some(text) = &self.line_text {
builder.count(text.as_ref().into_str());
}
Expand All @@ -727,7 +727,7 @@ impl Location {
// single-buffer packing.
Location {
file: Cow::Owned(self.file.to_vec()),
namespace: self.namespace,
namespace: Cow::Owned(self.namespace.to_vec()),
line: self.line,
column: self.column,
length: self.length,
Expand All @@ -748,7 +748,7 @@ impl Location {
) -> Location {
Location {
file: Cow::Borrowed(file),
namespace,
namespace: Cow::Borrowed(namespace),
line,
column,
length: length as usize,
Expand Down Expand Up @@ -781,7 +781,7 @@ impl Location {
if r.is_empty() {
return Some(Location {
file: Cow::Borrowed(source.path.text),
namespace: source.path.namespace,
namespace: Cow::Borrowed(source.path.namespace),
line: -1,
column: -1,
length: 0,
Expand Down Expand Up @@ -816,7 +816,7 @@ impl Location {

return Some(Location {
file: Cow::Borrowed(source.path.text),
namespace: source.path.namespace,
namespace: Cow::Borrowed(source.path.namespace),
line: usize2loc(data.line_count).start,
column: usize2loc(data.column_count).start,
length: if r.len > -1 {
Expand Down
2 changes: 1 addition & 1 deletion src/css/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ impl ErrorLocation {
.map(|lines| unsafe { &*std::ptr::from_ref::<[u8]>(lines.as_slice()[0]) });
Ok(bun_ast::Location {
file: std::borrow::Cow::Borrowed(source.path.text),
namespace: source.path.namespace,
namespace: std::borrow::Cow::Borrowed(source.path.namespace),
line: i32::try_from(self.line + 1).expect("int cast"),
column: i32::try_from(self.column).expect("int cast"),
line_text: line_text.map(std::borrow::Cow::Borrowed),
Expand Down
2 changes: 1 addition & 1 deletion src/jsc/BuildMessage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ impl BuildMessage {
object.put(
global,
b"namespace",
bun_string_jsc::create_utf8_for_js(global, location.namespace)?,
bun_string_jsc::create_utf8_for_js(global, &location.namespace)?,
);
object.put(global, b"line", JSValue::from(location.line));
object.put(global, b"column", JSValue::from(location.column));
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/server/DevErrorPage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ fn write_message_data(w: &mut Vec<u8>, text: &[u8], location: Option<&Location>)
w.extend_from_slice(b",\"location\":{\"file\":");
write_string(w, &location.file);
w.extend_from_slice(b",\"namespace\":");
write_string(w, location.namespace);
write_string(w, &location.namespace);
w.extend_from_slice(b",\"line_text\":");
write_string(w, location.line_text.as_deref().unwrap_or(b""));
write!(
Expand Down
51 changes: 51 additions & 0 deletions test/bundler/bun-build-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1908,6 +1908,57 @@ test("Bun.build does not corrupt folded string ropes shared across chunks", asyn
expect(exitCode).toBe(0);
}, 180_000);

// A plugin module's namespace lives in the bundle's arena. The `BuildMessage`
// objects in `result.logs` outlive the arena, so they must own a copy.
// MIMALLOC_PURGE_DELAY=0 makes mimalloc return the arena's pages to the OS as
// soon as the bundle ends, so a stale pointer crashes instead of reading the
// old bytes.
test.concurrent("a BuildMessage keeps the namespace of a plugin module after the build", async () => {
using dir = tempDir("build-message-namespace", {
"entry.ts": `import "virtual:broken";`,
"run.ts": `
const result = await Bun.build({
entrypoints: ["./entry.ts"],
throw: false,
plugins: [{
name: "virtual",
setup(builder) {
builder.onResolve({ filter: /^virtual:/ }, args => ({
path: args.path.slice("virtual:".length),
namespace: "virtual",
}));
builder.onLoad({ filter: /.*/, namespace: "virtual" }, () => ({
contents: "let = ;",
loader: "js",
}));
},
}],
});
console.log(JSON.stringify({
success: result.success,
positions: result.logs.map(log => {
const { file, namespace } = log.position!;
return { file, namespace };
}),
}));
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run.ts"],
cwd: String(dir),
env: { ...bunEnv, MIMALLOC_PURGE_DELAY: "0", MIMALLOC_ABANDONED_PAGE_PURGE: "1" },
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual({
success: false,
positions: [{ file: "broken", namespace: "virtual" }],
});
expect(exitCode).toBe(0);
});

test("sourcemap sourcesContent is valid JSON when source contains C0 control chars", async () => {
// RFC 8259 only allows \" \\ \/ \b \f \n \r \t and six-char \u escapes; \v
// and \xNN are JavaScript-only. A VT (0x0B) or BEL (0x07) in the input used
Expand Down
Loading