mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
Fix oom handling in Bun.file (#13603)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user