From 490685222ac9861d38ccdca6d92de7776303453e Mon Sep 17 00:00:00 2001 From: robobun Date: Tue, 5 May 2026 00:37:33 +0000 Subject: [PATCH 1/4] Fix use-after-free in Bun.Transpiler async transform() errors TransformTask.run() uses a per-task MimallocArena for parsing. When the parser emits diagnostics, the message text and notes slices are allocated from that arena. The arena is destroyed at the end of run(), but the log messages are read later on the main thread in then() via log.toJS(), leading to a use-after-poison / use-after-free. Deep-clone any accumulated log messages into bun.default_allocator before the arena is torn down so then() can safely surface them. --- src/runtime/api/JSTranspiler.zig | 3 ++ .../transpiler-async-error-uaf.test.ts | 44 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 test/js/bun/transpiler/transpiler-async-error-uaf.test.ts diff --git a/src/runtime/api/JSTranspiler.zig b/src/runtime/api/JSTranspiler.zig index 7d231902efe..7a2860f53ec 100644 --- a/src/runtime/api/JSTranspiler.zig +++ b/src/runtime/api/JSTranspiler.zig @@ -503,6 +503,9 @@ pub const TransformTask = struct { this.transpiler.setAllocator(allocator); this.transpiler.setLog(&this.log); this.log.msgs.allocator = bun.default_allocator; + defer for (this.log.msgs.items) |*msg| { + msg.* = msg.clone(bun.default_allocator) catch msg.*; + }; const jsx = if (this.tsconfig != null) this.tsconfig.?.mergeJSX(this.transpiler.options.jsx) diff --git a/test/js/bun/transpiler/transpiler-async-error-uaf.test.ts b/test/js/bun/transpiler/transpiler-async-error-uaf.test.ts new file mode 100644 index 00000000000..ffbffed6a97 --- /dev/null +++ b/test/js/bun/transpiler/transpiler-async-error-uaf.test.ts @@ -0,0 +1,44 @@ +import { expect, test } from "bun:test"; + +test("concurrent async transform() rejections do not use-after-free", async () => { + const transpiler = new Bun.Transpiler({ loader: "tsx" }); + + const inputs = [ + "const {a, a} = b", + "class X { @invalid }", + "const x: string = ;", + "@#$%^ invalid syntax !!!", + "function (", + ]; + + const promises: Promise[] = []; + for (let i = 0; i < 20; i++) { + for (const input of inputs) { + promises.push(transpiler.transform(input).catch(e => e)); + } + } + + const results = await Promise.all(promises); + expect(results).toHaveLength(20 * inputs.length); + for (const result of results) { + expect(result).toBeDefined(); + expect(typeof result).toBe("object"); + } +}); + +test("async transform() error preserves message and notes", async () => { + const transpiler = new Bun.Transpiler({ loader: "tsx" }); + + const errors = await Promise.all( + Array.from({ length: 8 }, () => transpiler.transform("const {a, a} = b").catch(e => e)), + ); + + for (const err of errors) { + expect(err.message).toBe('"a" has already been declared'); + expect(err.position?.file).toBe("input.tsx"); + expect(err.position?.lineText).toBe("const {a, a} = b"); + expect(err.notes).toHaveLength(1); + expect(err.notes[0].message).toBe('"a" was originally declared here'); + expect(err.notes[0].position?.lineText).toBe("const {a, a} = b"); + } +}); From 0a32150acaab373ba6078f99ca656fbcae7cbeb3 Mon Sep 17 00:00:00 2001 From: robobun Date: Tue, 5 May 2026 00:46:18 +0000 Subject: [PATCH 2/4] re-trigger gate From 0ec470217a27b5b64269700a98b63ce9bb1835f7 Mon Sep 17 00:00:00 2001 From: robobun Date: Tue, 5 May 2026 01:05:29 +0000 Subject: [PATCH 3/4] Use bun.handleOom and free cloned log messages in deinit --- src/runtime/api/JSTranspiler.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/runtime/api/JSTranspiler.zig b/src/runtime/api/JSTranspiler.zig index 7a2860f53ec..d05877a4d66 100644 --- a/src/runtime/api/JSTranspiler.zig +++ b/src/runtime/api/JSTranspiler.zig @@ -504,7 +504,7 @@ pub const TransformTask = struct { this.transpiler.setLog(&this.log); this.log.msgs.allocator = bun.default_allocator; defer for (this.log.msgs.items) |*msg| { - msg.* = msg.clone(bun.default_allocator) catch msg.*; + msg.* = bun.handleOom(msg.clone(bun.default_allocator)); }; const jsx = if (this.tsconfig != null) @@ -590,6 +590,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(); From e2618c92c2d10985d4867c2c1afc76143a25f383 Mon Sep 17 00:00:00 2001 From: robobun Date: Tue, 5 May 2026 01:19:25 +0000 Subject: [PATCH 4/4] retry gate