Fix oom handling in Bun.file (#13603)

This commit is contained in:
Jarred Sumner
2024-08-29 18:54:33 -07:00
committed by GitHub
parent f3ed9eac4a
commit bd2eb40a39
3 changed files with 140 additions and 78 deletions

View File

@@ -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;

View File

@@ -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));

View File

@@ -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();
});
});