Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/jsc/bindings/ImportMetaObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
#include "wtf/text/StringView.h"

#include "isBuiltinModule.h"
#include "VectorSizeLimit.h"
#include "WebCoreJSBuiltins.h"

namespace Zig {
Expand Down Expand Up @@ -289,7 +290,12 @@ extern "C" JSC::EncodedJSValue functionImportMeta__resolveSyncPrivate(JSC::JSGlo
WTF::String pathStr = path.toWTFString(globalObject);
if (scope.exception()) [[unlikely]]
goto cleanup;
paths.append(Bun::toStringRef(pathStr));
BunString pathRef = Bun::toStringRef(pathStr);
if (!Bun::tryAppendWithinLimit(paths, pathRef)) [[unlikely]] {
pathRef.deref();
throwOutOfMemoryError(globalObject, scope);
goto cleanup;
}
}

result = Bun__resolveSyncWithPaths(lexicalGlobalObject, JSC::JSValue::encode(moduleName), JSValue::encode(from), isESM, isRequireDotResolve, paths.begin(), paths.size());
Expand Down
13 changes: 13 additions & 0 deletions src/jsc/bindings/VectorSizeLimit.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,17 @@ size_t maxDequeSize()
return std::max<size_t>(std::bit_floor(maxVectorSize<T>()), 1) - 1;
}

// Fallible append and reserve for a Vector that script sizes. False when the request passes maxVectorSize or the allocation fails, and the caller throws.
template<typename T, size_t inlineCapacity, typename OverflowHandler, size_t minCapacity, typename Malloc, typename U>
bool tryAppendWithinLimit(WTF::Vector<T, inlineCapacity, OverflowHandler, minCapacity, Malloc>& vector, U&& value)
{
return vector.size() < maxVectorSize<T>() && vector.tryAppend(std::forward<U>(value));
}

template<typename T, size_t inlineCapacity, typename OverflowHandler, size_t minCapacity, typename Malloc>
bool tryReserveCapacityWithinLimit(WTF::Vector<T, inlineCapacity, OverflowHandler, minCapacity, Malloc>& vector, size_t capacity)
{
return capacity <= maxVectorSize<T>() && vector.tryReserveCapacity(capacity);
}

} // namespace Bun
5 changes: 4 additions & 1 deletion src/jsc/bindings/webcore/WorkerMessagingProxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
#include "MessageEvent.h"
#include "MessagePort.h"
#include "SerializedScriptValue.h"
#include "VectorSizeLimit.h"
#include "Worker.h"
#include "ZigGlobalObject.h"
#include <JavaScriptCore/JSPromise.h>
Expand Down Expand Up @@ -121,8 +122,10 @@ ExceptionOr<void> WorkerMessagingProxy::startWorkerGlobalScope(const String& scr
return {};
}

// Script picks how many preload entries there are, so the reserve is fallible.
Vector<BunString> preloadModules;
preloadModules.reserveInitialCapacity(m_options.preloadModules.size());
if (!Bun::tryReserveCapacityWithinLimit(preloadModules, m_options.preloadModules.size())) [[unlikely]]
return Exception { OutOfMemoryError };
for (auto& str : m_options.preloadModules) {
if (str.startsWith("file://"_s)) {
WTF::URL urlObject = WTF::URL(str);
Expand Down
4 changes: 2 additions & 2 deletions src/jsc/bindings/webcore/streams/BunStandaloneTextSink.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ struct BunTextAccumulator {
{
using Piece = JSC::WriteBarrier<JSC::Unknown>;
if (flushedRope) {
if (pieces.size() >= Bun::maxVectorSize<Piece>() || !pieces.tryAppend(Piece(vm, owner, flushedRope))) [[unlikely]]
if (!Bun::tryAppendWithinLimit(pieces, Piece(vm, owner, flushedRope))) [[unlikely]]
return false;
rope.clear();
}
return pieces.size() < Bun::maxVectorSize<Piece>() && pieces.tryAppend(Piece(vm, owner, chunk));
return Bun::tryAppendWithinLimit(pieces, Piece(vm, owner, chunk));
}

// Releases everything accumulated. Called as soon as the final result string has
Expand Down
2 changes: 1 addition & 1 deletion src/jsc/bindings/webcore/streams/BunStreamConsumers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ static JSValue concatenateChunks(JSC::VM& vm, JSGlobalObject* globalObject, JSAr
MarkedArgumentBuffer values;
WTF::Vector<std::pair<WTF::String, size_t>, 16> stringChunks;
// Script picks the chunk count here and the byte total below, so both reserves are fallible.
if (length > Bun::maxVectorSize<std::pair<WTF::String, size_t>>() || !stringChunks.tryReserveCapacity(length)) [[unlikely]] {
if (!Bun::tryReserveCapacityWithinLimit(stringChunks, length)) [[unlikely]] {
throwOutOfMemoryError(globalObject, scope);
return {};
}
Expand Down
7 changes: 6 additions & 1 deletion src/jsc/modules/NodeModuleModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "ZigGlobalObject.h"
#include "headers.h"
#include "ErrorCode.h"
#include "VectorSizeLimit.h"
#include "BunString.h"

#include "GeneratedNodeModuleModule.h"
Expand Down Expand Up @@ -361,7 +362,11 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionResolveFileName,
if (scope.exception())
return;

paths.append(Bun::toStringRef(pathStr));
BunString pathRef = Bun::toStringRef(pathStr);
if (!Bun::tryAppendWithinLimit(paths, pathRef)) [[unlikely]] {
pathRef.deref();
throwOutOfMemoryError(lexicalGlobalObject, scope);
}
});

if (scope.exception()) {
Expand Down
48 changes: 48 additions & 0 deletions test/js/node/module/node-module-module.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,54 @@ console.log("survived", require("./late.js"));`,
expect(Module._resolveFilename("fs", null, false, { paths: null })).toBe("fs");
});

// Script picks how many entries options.paths has, and each one is copied into a native list. A list that
// cannot grow must throw a catchable error, never abort the process. The real bound is 77,731,542 entries
// (5 GB), so the child runs with a 64 KiB synthetic allocation limit: 2730 entries of 24 bytes.
test("options.paths with more entries than the native list holds throws a RangeError", async () => {
const LIMIT_BYTES = 64 * 1024;
const MAX_PATHS = Math.floor(LIMIT_BYTES / 24);
using dir = tempDir("resolve-paths-limit", {
"lib/node_modules/limit-pkg/package.json": JSON.stringify({ name: "limit-pkg", main: "index.js" }),
"lib/node_modules/limit-pkg/index.js": "module.exports = 1;",
"main.cjs": `
const path = require("node:path");
const Module = require("node:module");
const doors = {
"require.resolve": paths => require.resolve("limit-pkg", { paths }),
"Module._resolveFilename": paths => Module._resolveFilename("limit-pkg", module, false, { paths }),
};
// Only the last entry holds the package, so a result proves that the whole list reached the resolver.
const resolveWith = (resolve, count) => {
const paths = new Array(count).fill(path.join(__dirname, "missing"));
paths[count - 1] = path.join(__dirname, "lib");
try {
return path.relative(__dirname, resolve(paths));
} catch (e) {
return e.name + ": " + e.message;
}
};
const result = {};
for (const [name, resolve] of Object.entries(doors)) {
result[name] = { atLimit: resolveWith(resolve, ${MAX_PATHS}), onePast: resolveWith(resolve, ${MAX_PATHS + 1}) };
}
console.log(JSON.stringify(result));
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), path.join(String(dir), "main.cjs")],
env: { ...bunEnv, BUN_FEATURE_FLAG_SYNTHETIC_MEMORY_LIMIT: String(LIMIT_BYTES) },
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const resolved = path.join("lib", "node_modules", "limit-pkg", "index.js");
expect(stderr).toBe("");
expect(JSON.parse(stdout || "null")).toEqual({
"require.resolve": { atLimit: resolved, onePast: "RangeError: Out of memory" },
"Module._resolveFilename": { atLimit: resolved, onePast: "RangeError: Out of memory" },
});
expect(exitCode).toBe(0);
});

test("createRequire trailing slash", () => {
const req = createRequire(import.meta.dir + "/");
expect(req.resolve("./node-module-module.test.js")).toBe(
Expand Down
37 changes: 37 additions & 0 deletions test/js/web/workers/worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,43 @@ describe("web worker", () => {
).toThrow(/Invalid file URL/);
});

// Script picks how many entries preload has, and each one is copied into a native list. A list that cannot
// hold them must throw a catchable error, never abort the process. The real bound is 89,478,485 entries
// (2.5 GB), so the child runs with a 64 KiB synthetic allocation limit: 2730 entries of 24 bytes.
test("more entries than the native list holds throws a RangeError", async () => {
const LIMIT_BYTES = 64 * 1024;
const MAX_ENTRIES = Math.floor(LIMIT_BYTES / 24);
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
// The last entry is an invalid file URL. A list that fits throws for that entry, so no thread starts.
const construct = count => {
const preload = new Array(count).fill("./preload.js");
preload[count - 1] = "file://:!:!:!!!!";
try {
new Worker("./entry.js", { preload });
return "constructed";
} catch (e) {
return e.name + ": " + e.message;
}
};
console.log(JSON.stringify({ atLimit: construct(${MAX_ENTRIES}), onePast: construct(${MAX_ENTRIES + 1}) }));
`,
],
env: { ...bunEnv, BUN_FEATURE_FLAG_SYNTHETIC_MEMORY_LIMIT: String(LIMIT_BYTES) },
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(JSON.parse(stdout || "null")).toEqual({
atLimit: 'TypeError: Invalid file URL: "file://:!:!:!!!!"',
onePast: "RangeError: Out of memory",
});
expect(exitCode).toBe(0);
});

test("string", async () => {
const worker = new Worker(new URL("worker-fixture-preload-entry.js", import.meta.url).href, {
preload: new URL("worker-fixture-preload.js", import.meta.url).href,
Expand Down
Loading