From 2d88bae336062ea981026b74882c8b271656e1a8 Mon Sep 17 00:00:00 2001 From: robobun Date: Mon, 4 May 2026 06:07:17 +0000 Subject: [PATCH 1/2] Fix use-after-free in Bun.Transpiler async transform error messages When transform() fails with parse errors, the error message text is allocated in a per-task arena that is destroyed before then() runs on the JS thread. Reading the log messages in then() accessed freed memory, producing corrupted error text in release builds and an ASAN use-after-poison crash in debug builds. Deep-clone the log messages into the default allocator before the arena is torn down so the error text survives until the promise is rejected. --- src/bun.js/api/JSTranspiler.zig | 4 +++ .../transpiler-transform-error-uaf.test.ts | 29 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 test/js/bun/transpiler/transpiler-transform-error-uaf.test.ts diff --git a/src/bun.js/api/JSTranspiler.zig b/src/bun.js/api/JSTranspiler.zig index 074a47e1558..42a8177e163 100644 --- a/src/bun.js/api/JSTranspiler.zig +++ b/src/bun.js/api/JSTranspiler.zig @@ -504,6 +504,10 @@ pub const TransformTask = struct { this.transpiler.setLog(&this.log); this.log.msgs.allocator = bun.default_allocator; + defer for (this.log.msgs.items) |*msg| { + msg.* = bun.handleOom(msg.clone(bun.default_allocator)); + }; + const jsx = if (this.tsconfig != null) this.tsconfig.?.mergeJSX(this.transpiler.options.jsx) else diff --git a/test/js/bun/transpiler/transpiler-transform-error-uaf.test.ts b/test/js/bun/transpiler/transpiler-transform-error-uaf.test.ts new file mode 100644 index 00000000000..2cb5c4f827c --- /dev/null +++ b/test/js/bun/transpiler/transpiler-transform-error-uaf.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from "bun:test"; + +test("concurrent async transform() with parse errors produces correct error messages", async () => { + const transpiler = new Bun.Transpiler(); + + const promises: Promise[] = []; + for (let i = 0; i < 100; i++) { + promises.push(transpiler.transform("const x = ;", "js").catch(e => e)); + } + + const errors = await Promise.all(promises); + for (const err of errors) { + expect(String(err)).toContain("Unexpected"); + } +}); + +test("concurrent async transform() with redeclaration errors produces correct error messages", async () => { + const transpiler = new Bun.Transpiler(); + + const promises: Promise[] = []; + for (let i = 0; i < 100; i++) { + promises.push(transpiler.transform("const x = 1; const x = 2;", "js").catch(e => e)); + } + + const errors = await Promise.all(promises); + for (const err of errors) { + expect(String(err)).toContain(`"x"`); + } +}); From 041626250664b1f98279888387b3fc4f5e545fcd Mon Sep 17 00:00:00 2001 From: robobun Date: Mon, 4 May 2026 06:14:36 +0000 Subject: [PATCH 2/2] Free cloned log message contents in TransformTask.deinit --- src/bun.js/api/JSTranspiler.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bun.js/api/JSTranspiler.zig b/src/bun.js/api/JSTranspiler.zig index 42a8177e163..fad25b92439 100644 --- a/src/bun.js/api/JSTranspiler.zig +++ b/src/bun.js/api/JSTranspiler.zig @@ -591,6 +591,7 @@ pub const TransformTask = struct { } pub fn deinit(this: *TransformTask) void { + for (this.log.msgs.items) |*msg| msg.deinit(bun.default_allocator); this.log.deinit(); this.input_code.deinitAndUnprotect(); this.output_code.deref();