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
121 changes: 121 additions & 0 deletions JSTests/stress/internal-function-new-without-construct-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// An InternalFunction subclass that takes JSCell::getConstructData keeps its functionForConstruct for `new f()`,
// and is not a constructor to anything that asks before it constructs.

function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error("expected " + String(expected) + " but got " + String(actual));
}

function shouldBeArray(actual, expected) {
shouldBe(Array.isArray(actual), true);
shouldBe(Object.getPrototypeOf(actual), Array.prototype);
shouldBe(JSON.stringify(actual), expected);
}

function shouldThrowTypeError(func) {
let error;
try {
func();
} catch (e) {
error = e;
}
if (!(error instanceof TypeError))
throw new Error("expected a TypeError but got " + String(error));
}

const f = $vm.createInternalFunctionWithoutConstructData();
const isConstructor = $vm.createBuiltin("(function (c) { return @isConstructor(c); })");

function constructWithNoArguments() { return new f(); }
function constructWithTwoArguments(a, b) { return new f(a, b); }
function constructWithSpread(args) { return new f(...args); }
function constructAny(constructor) { return new constructor(1); }
function callWithTwoArguments(a, b) { return f(a, b); }
function callWithSpread(args) { return f(...args); }
function queryIsConstructor() { return isConstructor(f); }
noInline(constructWithNoArguments);
noInline(constructWithTwoArguments);
noInline(constructWithSpread);
noInline(constructAny);
noInline(callWithTwoArguments);
noInline(callWithSpread);
noInline(queryIsConstructor);

class Plain { }

for (let i = 0; i < testLoopCount; ++i) {
shouldBe(constructWithNoArguments(), "new:0");
shouldBe(constructWithTwoArguments(1, 2), "new:2");
shouldBe(constructWithSpread([1, 2, 3]), "new:3");
shouldBe(callWithTwoArguments(1, 2), "call:2");
shouldBe(callWithSpread([1]), "call:1");
shouldBe(queryIsConstructor(), false);

// The same `new` site sees a class and this function.
shouldBe(constructAny(i & 1 ? f : Plain) instanceof Plain, !(i & 1));
}

class Species extends Array {
static get [Symbol.species]() { return f; }
}

class Reparented extends Array {
constructor() { super(); }
}
Object.setPrototypeOf(Reparented, f);

class ReparentedWithField extends Array {
field = 1;
}
Object.setPrototypeOf(ReparentedWithField, f);

function doors() {
shouldBe(typeof f, "function");
shouldBe(isConstructor(f), false);
shouldBe(isConstructor(f.bind(null)), false);
shouldBe(isConstructor(new Proxy(f, {})), false);

// A |this| that is not a constructor gets a plain array.
shouldBeArray(Array.of.call(f, 1, 2, 3), "[1,2,3]");
shouldBeArray(Array.of.call(f), "[]");
shouldBeArray(Array.from.call(f, [1, 2, 3]), "[1,2,3]");
shouldBeArray(Array.from.call(f, { length: 2, 0: "a", 1: "b" }), '["a","b"]');

const species = new Species(1, 2, 3);
shouldThrowTypeError(() => species.filter(() => true));
shouldThrowTypeError(() => species.map(x => x));
shouldThrowTypeError(() => species.flatMap(x => [x]));
shouldThrowTypeError(() => species.slice());
shouldThrowTypeError(() => species.splice(0, 1));
shouldThrowTypeError(() => species.concat([4]));
shouldThrowTypeError(() => species.flat());

const ownConstructor = [1, 2, 3];
ownConstructor.constructor = { [Symbol.species]: f };
shouldThrowTypeError(() => ownConstructor.map(x => x));

shouldThrowTypeError(() => Reflect.construct(f, []));
shouldThrowTypeError(() => Reflect.construct(f, [], Object));
shouldThrowTypeError(() => Reflect.construct(Object, [], f));
shouldThrowTypeError(() => new (f.bind(null))());
shouldThrowTypeError(() => new (new Proxy(f, {}))());
shouldThrowTypeError(() => { class Derived extends f { } });
shouldThrowTypeError(() => Promise.resolve.call(f, 1));
shouldThrowTypeError(() => Uint8Array.from.call(f, []));
shouldThrowTypeError(() => Uint8Array.of.call(f));

// bind and Proxy keep the call.
shouldBe(f.bind(null, 1)(2), "call:2");
shouldBe(new Proxy(f, {})(1, 2, 3), "call:3");

// `super()` reads no construct data. It runs functionForConstruct with the class as new.target, and
// functionForConstruct of this function throws for a new.target that is not the function.
shouldThrowTypeError(() => new Reparented());
shouldThrowTypeError(() => new ReparentedWithField());
shouldThrowTypeError(() => Array.of.call(Reparented, 1, 2, 3));
shouldThrowTypeError(() => Reflect.construct(Reparented, []));
}
noInline(doors);

for (let i = 0; i < 1e3; ++i)
doors();
9 changes: 9 additions & 0 deletions Source/JavaScriptCore/runtime/InternalFunction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,16 @@ InternalFunction::InternalFunction(VM& vm, Structure* structure, NativeFunction
ASSERT(is<InternalFunction>(this));
// JSCell::{getCallData,getConstructData} relies on the following conditions.
ASSERT(methodTable()->getCallData == InternalFunction::info()->methodTable.getCallData);
#if USE(BUN_JSC_ADDITIONS)
// A subclass whose functionForConstruct does not return an object takes JSCell::getConstructData with a using-declaration.
// It is then a constructor to nothing that reads construct data: isConstructor(), construct(), Reflect.construct, bind,
// Proxy, Array.of, Array.from, a species lookup. `new f()` and `super()` read none: they still run functionForConstruct,
// which has to check new.target.
ASSERT(methodTable()->getConstructData == InternalFunction::info()->methodTable.getConstructData
|| methodTable()->getConstructData == JSCell::getConstructData);
#else
ASSERT(methodTable()->getConstructData == InternalFunction::info()->methodTable.getConstructData);
#endif
ASSERT(type() == InternalFunctionType || type() == NullSetterFunctionType);
}

Expand Down
3 changes: 3 additions & 0 deletions Source/JavaScriptCore/runtime/JSCellInlines.h
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,9 @@ inline TriState JSCell::isConstructorWithConcurrency()
return (methodTable()->getConstructData(this).type != CallData::Type::None) ? TriState::True : TriState::False;
// We know that both getConstructData of both types are concurrency aware. Plus, derived classes of JSFunction and InternalFunction
// never override getConstructData (this is ensured by ASSERT in JSFunction and InternalFunction).
#if USE(BUN_JSC_ADDITIONS)
// Bun: a derived class of InternalFunction can take JSCell::getConstructData, which is concurrency aware too.
#endif
if (type() == JSFunctionType || type() == InternalFunctionType)
return (methodTable()->getConstructData(this).type != CallData::Type::None) ? TriState::True : TriState::False;
return TriState::Indeterminate;
Expand Down
66 changes: 66 additions & 0 deletions Source/JavaScriptCore/tools/JSDollarVM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
#include "WasmCapabilities.h"
#if USE(BUN_JSC_ADDITIONS)
#include "BufferAccessorRegistry.h"
#include "FunctionPrototype.h"
#include "JSArrayBufferView.h"
#include "JSBigInt.h"
#include "MathCommon.h"
Expand Down Expand Up @@ -2160,6 +2161,63 @@ JSC_DEFINE_HOST_FUNCTION(functionWasmStreamingCompilerAddBytes, (JSGlobalObject*

#endif

#if USE(BUN_JSC_ADDITIONS)
static JSC_DECLARE_HOST_FUNCTION(callInternalFunctionWithoutConstructData);
static JSC_DECLARE_HOST_FUNCTION(constructInternalFunctionWithoutConstructData);

// Both handlers return a string, the way an embedder's function does when `new f()` has to evaluate to a primitive.
class InternalFunctionWithoutConstructData final : public InternalFunction {
public:
using Base = InternalFunction;
using JSCell::getConstructData;

DECLARE_INFO;

static Structure* createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
{
DollarVMAssertScope assertScope;
return Structure::create(vm, globalObject, prototype, TypeInfo(InternalFunctionType, StructureFlags), info());
}

static InternalFunctionWithoutConstructData* create(VM& vm, JSGlobalObject* globalObject)
{
DollarVMAssertScope assertScope;
Structure* structure = createStructure(vm, globalObject, globalObject->functionPrototype());
auto* function = new (NotNull, allocateCell<InternalFunctionWithoutConstructData>(vm)) InternalFunctionWithoutConstructData(vm, structure);
function->finishCreation(vm, 0, "InternalFunctionWithoutConstructData"_s, PropertyAdditionMode::WithoutStructureTransition);
return function;
}

private:
InternalFunctionWithoutConstructData(VM& vm, Structure* structure)
: Base(vm, structure, callInternalFunctionWithoutConstructData, constructInternalFunctionWithoutConstructData)
{
DollarVMAssertScope assertScope;
}
};

const ClassInfo InternalFunctionWithoutConstructData::s_info = { "InternalFunctionWithoutConstructData"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(InternalFunctionWithoutConstructData) };

JSC_DEFINE_HOST_FUNCTION(callInternalFunctionWithoutConstructData, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
DollarVMAssertScope assertScope;
return JSValue::encode(jsString(globalObject->vm(), makeString("call:"_s, callFrame->argumentCount())));
}

JSC_DEFINE_HOST_FUNCTION(constructInternalFunctionWithoutConstructData, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
DollarVMAssertScope assertScope;
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);

// `super()` of a class whose [[Prototype]] was set to this function gets here with the class as new.target.
if (callFrame->newTarget() != callFrame->jsCallee()) [[unlikely]]
return throwVMTypeError(globalObject, scope, "InternalFunctionWithoutConstructData is not a constructor"_s);

return JSValue::encode(jsString(vm, makeString("new:"_s, callFrame->argumentCount())));
}
#endif

} // namespace

namespace JSC {
Expand Down Expand Up @@ -2355,6 +2413,7 @@ static JSC_DECLARE_HOST_FUNCTION(functionFFIWrite);
static JSC_DECLARE_HOST_FUNCTION(functionFFICString);
static JSC_DECLARE_HOST_FUNCTION(functionFFIArenaDepth);
static JSC_DECLARE_HOST_FUNCTION(functionFFICompileCounts);
static JSC_DECLARE_HOST_FUNCTION(functionCreateInternalFunctionWithoutConstructData);
#endif

const ClassInfo JSDollarVM::s_info = { "DollarVM"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSDollarVM) };
Expand Down Expand Up @@ -5514,6 +5573,12 @@ JSC_DEFINE_HOST_FUNCTION(functionFFICompileCounts, (JSGlobalObject* globalObject
return JSValue::encode(counts);
}

JSC_DEFINE_HOST_FUNCTION(functionCreateInternalFunctionWithoutConstructData, (JSGlobalObject* globalObject, CallFrame*))
{
DollarVMAssertScope assertScope;
return JSValue::encode(InternalFunctionWithoutConstructData::create(globalObject->vm(), globalObject));
}

#endif // USE(BUN_JSC_ADDITIONS)

#if USE(BUN_JSC_ADDITIONS)
Expand Down Expand Up @@ -6109,6 +6174,7 @@ void JSDollarVM::finishCreation(VM& vm)
addFunction(vm, allowIfNotFuzz, "ffiCString"_s, functionFFICString, 1);
addFunction(vm, allowIfNotFuzz, "ffiArenaDepth"_s, functionFFIArenaDepth, 0);
addFunction(vm, allowIfNotFuzz, "ffiCompileCounts"_s, functionFFICompileCounts, 0);
addFunction(vm, allowIfNotFuzz, "createInternalFunctionWithoutConstructData"_s, functionCreateInternalFunctionWithoutConstructData, 0);
#endif

if (allowIfNotFuzz) {
Expand Down
Loading