Skip to content
Open
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
39 changes: 27 additions & 12 deletions src/jsc/bindings/NodeVM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -108,23 +108,30 @@ static JSValue scriptFetchParametersToImportAttributes(JSGlobalObject* globalObj
return obj;
}

bool extractCachedData(JSValue cachedDataValue, WTF::Vector<uint8_t>& outCachedData)
CachedDataExtraction extractCachedData(JSValue cachedDataValue, WTF::Vector<uint8_t>& outCachedData)
{
if (!cachedDataValue.isCell()) {
return false;
return CachedDataExtraction::NotABuffer;
}

std::span<const uint8_t> bytes;
if (auto* arrayBufferView = dynamicDowncast<JSC::JSArrayBufferView>(cachedDataValue)) {
if (!arrayBufferView->isDetached()) {
outCachedData = arrayBufferView->span();
return true;
if (arrayBufferView->isDetached()) {
return CachedDataExtraction::NotABuffer;
}
bytes = arrayBufferView->span();
} else if (auto* arrayBuffer = dynamicDowncast<JSC::JSArrayBuffer>(cachedDataValue); arrayBuffer && arrayBuffer->impl()) {
outCachedData = arrayBuffer->impl()->toVector();
return true;
bytes = arrayBuffer->impl()->span();
} else {
return CachedDataExtraction::NotABuffer;
}

return false;
if (!WTF::isValidCapacityForVector<uint8_t>(bytes.size())) {
return CachedDataExtraction::TooLong;
}

outCachedData = bytes;
return CachedDataExtraction::Copied;
}

JSC::JSFunction* constructAnonymousFunction(JSC::JSGlobalObject* globalObject, const ArgList& args, const SourceOrigin& sourceOrigin, CompileFunctionOptions&& options, JSC::SourceTaintedOrigin sourceTaintOrigin, JSC::JSScope* scope)
Expand Down Expand Up @@ -218,7 +225,9 @@ JSC::JSFunction* constructAnonymousFunction(JSC::JSGlobalObject* globalObject, c

TriState bytecodeAccepted = TriState::Indeterminate;

if (!options.cachedData.isEmpty()) {
if (options.cachedDataTooLong) {
bytecodeAccepted = TriState::False;
} else if (!options.cachedData.isEmpty()) {
cachedBytecode = CachedBytecode::create(std::span(options.cachedData), nullptr, {});
SourceCodeKey key(sourceCode, {}, JSC::SourceCodeType::ProgramType, lexicallyScopedFeatures, JSC::JSParserScriptMode::Classic, JSC::DerivedContextType::None, JSC::EvalContextType::None, false, {}, std::nullopt);
unlinkedProgramCodeBlock = JSC::decodeCodeBlock<UnlinkedProgramCodeBlock>(vm, key, *cachedBytecode);
Expand Down Expand Up @@ -1898,15 +1907,21 @@ bool BaseVMOptions::validateProduceCachedData(JSC::JSGlobalObject* globalObject,
return false;
}

bool BaseVMOptions::validateCachedData(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSObject* options, WTF::Vector<uint8_t>& outCachedData)
bool BaseVMOptions::validateCachedData(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSObject* options, WTF::Vector<uint8_t>& outCachedData, bool& outCachedDataTooLong)
{
JSValue cachedDataOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "cachedData"_s));
RETURN_IF_EXCEPTION(scope, {});

if (cachedDataOpt && !cachedDataOpt.isUndefined()) {
// Verify it's a Buffer, TypedArray or DataView and extract the data if it is.
if (extractCachedData(cachedDataOpt, outCachedData)) {
switch (extractCachedData(cachedDataOpt, outCachedData)) {
case CachedDataExtraction::Copied:
return true;
case CachedDataExtraction::TooLong:
outCachedDataTooLong = true;
return true;
case CachedDataExtraction::NotABuffer:
break;
}

ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.cachedData"_s, "Buffer, TypedArray, or DataView"_s, cachedDataOpt);
Expand Down Expand Up @@ -1960,7 +1975,7 @@ bool CompileFunctionOptions::fromJS(JSC::JSGlobalObject* globalObject, JSC::VM&
// The validators return false both for "absent" and for "threw".
RETURN_IF_EXCEPTION(scope, false);

if (validateCachedData(globalObject, vm, scope, options, this->cachedData))
if (validateCachedData(globalObject, vm, scope, options, this->cachedData, this->cachedDataTooLong))
any = true;
RETURN_IF_EXCEPTION(scope, false);

Expand Down
15 changes: 13 additions & 2 deletions src/jsc/bindings/NodeVM.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,16 @@ class CompileFunctionOptions;
namespace NodeVM {

RefPtr<JSC::CachedBytecode> getBytecode(JSGlobalObject* globalObject, JSC::SourceCodeType, const JSC::SourceCode& source);
bool extractCachedData(JSValue cachedDataValue, WTF::Vector<uint8_t>& outCachedData);
enum class CachedDataExtraction : uint8_t {
// Not a Buffer, TypedArray, DataView or ArrayBuffer, or a detached view.
NotABuffer,
Copied,
// Longer than a WTF::Vector holds (INT_MAX), so not copied. The caller reports the data as rejected, which is
// also Node's result unless the buffer starts with a valid cache (its length narrows to V8's `int` there):
// https://github.com/nodejs/node/blob/v26.3.0/src/node_contextify.cc#L1027-L1028
TooLong,
};
CachedDataExtraction extractCachedData(JSValue cachedDataValue, WTF::Vector<uint8_t>& outCachedData);
String stringifyAnonymousFunction(JSGlobalObject* globalObject, const ArgList& args, ThrowScope& scope, int* outOffset);
JSC::EncodedJSValue createCachedData(JSGlobalObject* globalObject, const JSC::SourceCode& source);
bool handleException(JSGlobalObject* globalObject, VM& vm, NakedPtr<JSC::Exception> exception, ThrowScope& throwScope);
Expand Down Expand Up @@ -59,7 +68,7 @@ class BaseVMOptions {

bool fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg);
bool validateProduceCachedData(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSObject* options, bool& outProduceCachedData);
bool validateCachedData(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSObject* options, WTF::Vector<uint8_t>& outCachedData);
bool validateCachedData(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSObject* options, WTF::Vector<uint8_t>& outCachedData, bool& outCachedDataTooLong);
bool validateTimeout(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSObject* options, std::optional<int64_t>& outTimeout);
};

Expand All @@ -69,6 +78,8 @@ class CompileFunctionOptions : public BaseVMOptions {
JSGlobalObject* parsingContext = nullptr;
JSValue contextExtensions {};
bool produceCachedData = false;
// See NodeVM::CachedDataExtraction::TooLong.
bool cachedDataTooLong = false;

using BaseVMOptions::BaseVMOptions;

Expand Down
6 changes: 4 additions & 2 deletions src/jsc/bindings/NodeVMScript.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ bool ScriptOptions::fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::
any = true;
RETURN_IF_EXCEPTION(scope, false);

if (validateCachedData(globalObject, vm, scope, options, this->cachedData))
if (validateCachedData(globalObject, vm, scope, options, this->cachedData, this->cachedDataTooLong))
any = true;
RETURN_IF_EXCEPTION(scope, false);

Expand Down Expand Up @@ -169,7 +169,9 @@ constructScript(JSGlobalObject* globalObject, CallFrame* callFrame, JSValue newT

WTF::Vector<uint8_t>& cachedData = script->cachedData();

if (!cachedData.isEmpty()) {
if (script->options().cachedDataTooLong) {
script->cachedDataRejected(TriState::True);
} else if (!cachedData.isEmpty()) {
JSC::ProgramExecutable* executable = script->cachedExecutable();
if (!executable) {
executable = script->createExecutable();
Expand Down
2 changes: 2 additions & 0 deletions src/jsc/bindings/NodeVMScript.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class ScriptOptions : public BaseVMOptions {
WTF::Vector<uint8_t> cachedData;
std::optional<int64_t> timeout = std::nullopt;
bool produceCachedData = false;
// See NodeVM::CachedDataExtraction::TooLong.
bool cachedDataTooLong = false;

using BaseVMOptions::BaseVMOptions;

Expand Down
22 changes: 18 additions & 4 deletions src/jsc/bindings/NodeVMSourceTextModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,18 @@ NodeVMSourceTextModule* NodeVMSourceTextModule::create(VM& vm, JSGlobalObject* g

JSValue cachedDataValue = args.at(5);
WTF::Vector<uint8_t> cachedData;
if (!cachedDataValue.isUndefined() && !extractCachedData(cachedDataValue, cachedData)) {
Bun::ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.cachedData"_s, "Buffer, TypedArray, or DataView"_s, cachedDataValue);
return nullptr;
bool cachedDataTooLong = false;
if (!cachedDataValue.isUndefined()) {
switch (extractCachedData(cachedDataValue, cachedData)) {
case CachedDataExtraction::Copied:
break;
case CachedDataExtraction::TooLong:
cachedDataTooLong = true;
break;
case CachedDataExtraction::NotABuffer:
Bun::ERR::INVALID_ARG_INSTANCE(scope, globalObject, "options.cachedData"_s, "Buffer, TypedArray, or DataView"_s, cachedDataValue);
return nullptr;
}
}

JSValue initializeImportMeta = args.at(6);
Expand Down Expand Up @@ -112,7 +121,7 @@ NodeVMSourceTextModule* NodeVMSourceTextModule::create(VM& vm, JSGlobalObject* g
WTF::move(sourceCode), moduleWrapper, initializeImportMeta);
ptr->finishCreation(vm);

if (cachedData.isEmpty()) {
if (cachedData.isEmpty() && !cachedDataTooLong) {
return ptr;
}

Expand All @@ -124,6 +133,11 @@ NodeVMSourceTextModule* NodeVMSourceTextModule::create(VM& vm, JSGlobalObject* g
return nullptr;
}

if (cachedDataTooLong) {
throwError(globalObject, scope, ErrorCode::ERR_VM_MODULE_CACHED_DATA_REJECTED, "cachedData buffer was rejected"_s);
return nullptr;
}

// Decoding checks the format and the source key. Linking would need
// the module's JSModuleEnvironment, which does not exist yet.
LexicallyScopedFeatures lexicallyScopedFeatures = StrictModeLexicallyScopedFeature;
Expand Down
53 changes: 53 additions & 0 deletions test/js/node/vm/vm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,59 @@ test("SourceTextModule accepts the cachedData it produced", () => {
);
});

test("cachedData of 2 GiB or more is reported as rejected instead of aborting", async () => {
// The child reserves 2 GiB of address space and never writes to it, so RSS stays small.
const fixture = `
const vm = require("node:vm");
let buffer;
try {
buffer = new ArrayBuffer(2 ** 31);
} catch {
console.log("SKIP");
process.exit(0);
}
const result = {};
// A bare ArrayBuffer is a Bun extension: Node takes views only.
for (const cachedData of [new Uint8Array(buffer), new DataView(buffer), buffer]) {
const script = new vm.Script("1 + 1", { cachedData });
const fn = vm.compileFunction("return 2 + 2", [], { cachedData });
result[cachedData.constructor.name] = {
Script: [script.cachedDataRejected, script.runInThisContext()],
compileFunction: [fn.cachedDataRejected, fn()],
};
}
try {
result.SourceTextModule = new vm.SourceTextModule("export default 1", { cachedData: new Uint8Array(buffer) }).status;
} catch (e) {
result.SourceTextModule = e.code;
}
console.log(JSON.stringify(result));
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", fixture],
env: {
...bunEnv,
ASAN_OPTIONS: [bunEnv.ASAN_OPTIONS, "allocator_may_return_null=1"].filter(Boolean).join(":"),
},
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// The child prints SKIP when it cannot reserve 2 GiB.
if (stdout.trim() !== "SKIP") {
const rejected = { Script: [true, 2], compileFunction: [true, 4] };
expect({ stdout: stdout && JSON.parse(stdout), stderr }).toEqual({
stdout: {
Uint8Array: rejected,
DataView: rejected,
ArrayBuffer: rejected,
SourceTextModule: "ERR_VM_MODULE_CACHED_DATA_REJECTED",
},
stderr: "",
});
}
expect(exitCode).toBe(0);
});

describe("Script compiles its source once and links that in every context it runs in", () => {
// Runs Script(s) in fresh contexts, keeping what every run produced alive (each run's wrapper function
// pins that run's ProgramExecutable), and reports how many UnlinkedProgramCodeBlock cells (one per
Expand Down
Loading