diff --git a/src/bun.js/webcore/blob.zig b/src/bun.js/webcore/blob.zig index 653c5731c7..f2ec86b337 100644 --- a/src/bun.js/webcore/blob.zig +++ b/src/bun.js/webcore/blob.zig @@ -4334,10 +4334,13 @@ pub const Blob = struct { pub fn toArrayBufferViewWithBytes(this: *Blob, global: *JSGlobalObject, buf: []u8, comptime lifetime: Lifetime, comptime TypedArrayView: JSC.JSValue.JSType) JSValue { switch (comptime lifetime) { .clone => { - if (buf.len > JSC.synthetic_allocation_limit) { - global.throwOutOfMemory(); - this.detach(); - return JSValue.zero; + if (TypedArrayView != .ArrayBuffer) { + // ArrayBuffer doesn't have this limit. + if (buf.len > JSC.synthetic_allocation_limit) { + global.throwOutOfMemory(); + this.detach(); + return JSValue.zero; + } } if (comptime Environment.isLinux) { @@ -4374,7 +4377,7 @@ pub const Blob = struct { return JSC.ArrayBuffer.create(global, buf, TypedArrayView); }, .share => { - if (buf.len > JSC.synthetic_allocation_limit) { + if (buf.len > JSC.synthetic_allocation_limit and TypedArrayView != .ArrayBuffer) { global.throwOutOfMemory(); return JSValue.zero; } @@ -4388,7 +4391,7 @@ pub const Blob = struct { ); }, .transfer => { - if (buf.len > JSC.synthetic_allocation_limit) { + if (buf.len > JSC.synthetic_allocation_limit and TypedArrayView != .ArrayBuffer) { global.throwOutOfMemory(); this.detach(); return JSValue.zero; @@ -4404,7 +4407,7 @@ pub const Blob = struct { ); }, .temporary => { - if (buf.len > JSC.synthetic_allocation_limit) { + if (buf.len > JSC.synthetic_allocation_limit and TypedArrayView != .ArrayBuffer) { global.throwOutOfMemory(); bun.default_allocator.free(buf); return JSValue.zero; diff --git a/src/bun.js/webcore/blob/ReadFile.zig b/src/bun.js/webcore/blob/ReadFile.zig index e4c3e03d8f..c723eea4a6 100644 --- a/src/bun.js/webcore/blob/ReadFile.zig +++ b/src/bun.js/webcore/blob/ReadFile.zig @@ -34,15 +34,14 @@ pub fn NewReadFileHandler(comptime Function: anytype) type { .result => |result| { const bytes = result.buf; if (blob.size > 0) - blob.size = @min(@as(u32, @truncate(bytes.len)), blob.size); - const value = Function(&blob, globalThis, bytes, .temporary); + blob.size = @min(@as(Blob.SizeType, @truncate(bytes.len)), blob.size); + const WrappedFn = struct { + pub fn wrapped(b: *Blob, g: *JSGlobalObject, by: []u8) JSC.JSValue { + return Function(b, g, by, .temporary); + } + }; - // invalid JSON needs to be rejected - if (value.isAnyError()) { - promise.reject(globalThis, value); - } else { - promise.resolve(globalThis, value); - } + JSC.AnyPromise.wrap(.{ .Normal = promise }, globalThis, WrappedFn.wrapped, .{ &blob, globalThis, bytes }); }, .err => |err| { promise.reject(globalThis, err.toErrorInstance(globalThis)); diff --git a/test/js/web/fetch/blob-oom.test.ts b/test/js/web/fetch/blob-oom.test.ts index ce8e0925b5..9d83181f01 100644 --- a/test/js/web/fetch/blob-oom.test.ts +++ b/test/js/web/fetch/blob-oom.test.ts @@ -1,84 +1,144 @@ -import { afterAll, afterEach, beforeAll, describe, expect, it, test } from "bun:test"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, test } from "bun:test"; import { setSyntheticAllocationLimitForTesting } from "bun:internal-for-testing"; -setSyntheticAllocationLimitForTesting(128 * 1024 * 1024); -afterEach(() => { - Bun.gc(true); -}); - -describe("Blob", () => { - let buf: ArrayBuffer; +import { tempDirWithFiles } from "harness"; +import { unlinkSync } from "fs"; +import path from "path"; +describe("Memory", () => { beforeAll(() => { - buf = new ArrayBuffer(Math.floor(64 * 1024 * 1024)); + setSyntheticAllocationLimitForTesting(128 * 1024 * 1024); + }); + afterEach(() => { + Bun.gc(true); }); - test(".json() should throw an OOM without crashing the process.", () => { - const array = [buf, buf, buf, buf, buf, buf, buf, buf, buf]; - expect(async () => await new Blob(array).json()).toThrow( - "Cannot parse a JSON string longer than 2^32-1 characters", - ); + describe("Blob", () => { + let buf: ArrayBuffer; + beforeAll(() => { + buf = new ArrayBuffer(Math.floor(64 * 1024 * 1024)); + }); + + test(".json() should throw an OOM without crashing the process.", () => { + const array = [buf, buf, buf, buf, buf, buf, buf, buf, buf]; + expect(async () => await new Blob(array).json()).toThrow( + "Cannot parse a JSON string longer than 2^32-1 characters", + ); + }); + + test(".text() should throw an OOM without crashing the process.", () => { + const array = [buf, buf, buf, buf, buf, buf, buf, buf, buf]; + expect(async () => await new Blob(array).text()).toThrow("Cannot create a string longer than 2^32-1 characters"); + }); + + test(".bytes() should throw an OOM without crashing the process.", () => { + const array = [buf, buf, buf, buf, buf, buf, buf, buf, buf]; + expect(async () => await new Blob(array).bytes()).toThrow("Out of memory"); + }); + + test(".arrayBuffer() should NOT throw an OOM.", () => { + const array = [buf, buf, buf, buf, buf, buf, buf, buf, buf]; + expect(async () => await new Blob(array).arrayBuffer()).not.toThrow(); + }); }); - test(".text() should throw an OOM without crashing the process.", () => { - const array = [buf, buf, buf, buf, buf, buf, buf, buf, buf]; - expect(async () => await new Blob(array).text()).toThrow("Cannot create a string longer than 2^32-1 characters"); + describe("Response", () => { + let blob: Blob; + beforeAll(() => { + const buf = new ArrayBuffer(Math.floor(64 * 1024 * 1024)); + blob = new Blob([buf, buf, buf, buf, buf, buf, buf, buf, buf]); + }); + afterAll(() => { + blob = undefined; + }); + + test(".text() should throw an OOM without crashing the process.", () => { + expect(async () => await new Response(blob).text()).toThrow( + "Cannot create a string longer than 2^32-1 characters", + ); + }); + + test(".bytes() should throw an OOM without crashing the process.", async () => { + expect(async () => await new Response(blob).bytes()).toThrow("Out of memory"); + }); + + test(".arrayBuffer() should NOT throw an OOM.", async () => { + expect(async () => await new Response(blob).arrayBuffer()).not.toThrow(); + }); + + test(".json() should throw an OOM without crashing the process.", async () => { + expect(async () => await new Response(blob).json()).toThrow( + "Cannot parse a JSON string longer than 2^32-1 characters", + ); + }); }); - test(".arrayBuffer() should throw an OOM without crashing the process.", () => { - const array = [buf, buf, buf, buf, buf, buf, buf, buf, buf]; - expect(async () => await new Blob(array).arrayBuffer()).toThrow("Out of memory"); + describe("Request", () => { + let blob: Blob; + beforeAll(() => { + const buf = new ArrayBuffer(Math.floor(64 * 1024 * 1024)); + blob = new Blob([buf, buf, buf, buf, buf, buf, buf, buf, buf]); + }); + afterAll(() => { + blob = undefined; + }); + + test(".text() should throw an OOM without crashing the process.", () => { + expect(async () => await new Request("http://localhost:3000", { body: blob }).text()).toThrow( + "Cannot create a string longer than 2^32-1 characters", + ); + }); + + test(".bytes() should throw an OOM without crashing the process.", async () => { + expect(async () => await new Request("http://localhost:3000", { body: blob }).bytes()).toThrow("Out of memory"); + }); + + test(".arrayBuffer() should NOT throw an OOM.", async () => { + expect(async () => await new Request("http://localhost:3000", { body: blob }).arrayBuffer()).not.toThrow(); + }); + + test(".json() should throw an OOM without crashing the process.", async () => { + expect(async () => await new Request("http://localhost:3000", { body: blob }).json()).toThrow( + "Cannot parse a JSON string longer than 2^32-1 characters", + ); + }); }); }); -describe("Response", () => { - let blob: Blob; - beforeAll(() => { - const buf = new ArrayBuffer(Math.floor(64 * 1024 * 1024)); - blob = new Blob([buf, buf, buf, buf, buf, buf, buf, buf, buf]); +describe("Bun.file", () => { + let tmpFile; + beforeAll(async () => { + const buf = Buffer.allocUnsafe(8 * 1024 * 1024); + const tmpDir = tempDirWithFiles("file-oom", { + "file.txt": buf, + }); + tmpFile = path.join(tmpDir, "file.txt"); + }); + beforeEach(() => { + setSyntheticAllocationLimitForTesting(4 * 1024 * 1024); + }); + afterEach(() => { + setSyntheticAllocationLimitForTesting(128 * 1024 * 1024); }); afterAll(() => { - blob = undefined; + try { + unlinkSync(tmpFile); + } catch (err) { + console.error(err); + } }); - test(".text() should throw an OOM without crashing the process.", () => { - expect(async () => await new Response(blob).text()).toThrow("Cannot create a string longer than 2^32-1 characters"); + test("text() should throw an OOM without crashing the process.", () => { + expect(async () => await Bun.file(tmpFile).text()).toThrow(); }); - test(".arrayBuffer() should throw an OOM without crashing the process.", async () => { - expect(async () => await new Response(blob).arrayBuffer()).toThrow("Out of memory"); + test("bytes() should throw an OOM without crashing the process.", () => { + expect(async () => await Bun.file(tmpFile).bytes()).toThrow(); }); - test(".json() should throw an OOM without crashing the process.", async () => { - expect(async () => await new Response(blob).json()).toThrow( - "Cannot parse a JSON string longer than 2^32-1 characters", - ); - }); -}); - -describe("Request", () => { - let blob: Blob; - beforeAll(() => { - const buf = new ArrayBuffer(Math.floor(64 * 1024 * 1024)); - blob = new Blob([buf, buf, buf, buf, buf, buf, buf, buf, buf]); - }); - afterAll(() => { - blob = undefined; - }); - - test(".text() should throw an OOM without crashing the process.", () => { - expect(async () => await new Request("http://localhost:3000", { body: blob }).text()).toThrow( - "Cannot create a string longer than 2^32-1 characters", - ); - }); - - test(".arrayBuffer() should throw an OOM without crashing the process.", async () => { - expect(async () => await new Request("http://localhost:3000", { body: blob }).arrayBuffer()).toThrow( - "Out of memory", - ); - }); - - test(".json() should throw an OOM without crashing the process.", async () => { - expect(async () => await new Request("http://localhost:3000", { body: blob }).json()).toThrow( - "Cannot parse a JSON string longer than 2^32-1 characters", - ); + test("json() should throw an OOM without crashing the process.", () => { + expect(async () => await Bun.file(tmpFile).json()).toThrow(); + }); + + test("arrayBuffer() should NOT throw an OOM.", () => { + expect(async () => await Bun.file(tmpFile).arrayBuffer()).not.toThrow(); }); });