diff --git a/src/ast/lib.rs b/src/ast/lib.rs index af50e2755ac7..3456860e472a 100644 --- a/src/ast/lib.rs +++ b/src/ast/lib.rs @@ -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>, /// Number of bytes this location should highlight. @@ -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::clone()` / `Vec::clone()` // could outlive the source buffer and read poisoned memory. Instead, @@ -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, @@ -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, @@ -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()); } @@ -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, @@ -748,7 +748,7 @@ impl Location { ) -> Location { Location { file: Cow::Borrowed(file), - namespace, + namespace: Cow::Borrowed(namespace), line, column, length: length as usize, @@ -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, @@ -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 { diff --git a/src/css/error.rs b/src/css/error.rs index bcefd472d809..70d16fa48773 100644 --- a/src/css/error.rs +++ b/src/css/error.rs @@ -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), diff --git a/src/jsc/BuildMessage.rs b/src/jsc/BuildMessage.rs index a56d6f0d0b6e..6d8f96577a47 100644 --- a/src/jsc/BuildMessage.rs +++ b/src/jsc/BuildMessage.rs @@ -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)); diff --git a/src/runtime/server/DevErrorPage.rs b/src/runtime/server/DevErrorPage.rs index ac8473b50089..53b2c98b5223 100644 --- a/src/runtime/server/DevErrorPage.rs +++ b/src/runtime/server/DevErrorPage.rs @@ -169,7 +169,7 @@ fn write_message_data(w: &mut Vec, 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!( diff --git a/test/bundler/bun-build-api.test.ts b/test/bundler/bun-build-api.test.ts index 98379f5ec6ae..c966ccff6213 100644 --- a/test/bundler/bun-build-api.test.ts +++ b/test/bundler/bun-build-api.test.ts @@ -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