diff --git a/src/runtime/api/JSTranspiler.zig b/src/runtime/api/JSTranspiler.zig index 7d231902efe..d05877a4d66 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.* = bun.handleOom(msg.clone(bun.default_allocator)); + }; const jsx = if (this.tsconfig != null) this.tsconfig.?.mergeJSX(this.transpiler.options.jsx) @@ -587,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(); 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"); + } +});