Bug 1494537: Add CacheIR stub for out-of-initialized-length-bounds assignments to arrays. r=tcampbell

This commit is contained in:
Kannan Vijayan 2018-10-17 14:48:25 -04:00
parent cd852e3dfb
commit 54485b65bc
15 changed files with 472 additions and 7 deletions

View File

@ -412,9 +412,6 @@ js::GetElementsWithAdder(JSContext* cx, HandleObject obj, HandleObject receiver,
return true;
}
static bool
ObjectMayHaveExtraIndexedProperties(JSObject* obj);
static inline bool
IsPackedArrayOrNoExtraIndexedProperties(JSObject* obj, uint64_t length)
{
@ -1050,8 +1047,8 @@ ObjectMayHaveExtraIndexedOwnProperties(JSObject* obj)
* elements. This includes other indexed properties in its shape hierarchy, and
* indexed properties or elements along its prototype chain.
*/
static bool
ObjectMayHaveExtraIndexedProperties(JSObject* obj)
bool
js::ObjectMayHaveExtraIndexedProperties(JSObject* obj)
{
MOZ_ASSERT_IF(obj->hasDynamicPrototype(), !obj->isNative());

View File

@ -196,6 +196,9 @@ array_construct(JSContext* cx, unsigned argc, Value* vp);
extern bool
IsCrossRealmArrayConstructor(JSContext* cx, const Value& v, bool* result);
extern bool
ObjectMayHaveExtraIndexedProperties(JSObject* obj);
class MOZ_NON_TEMPORARY_CLASS ArraySpeciesLookup final
{
/*

View File

@ -0,0 +1,125 @@
setJitCompilerOption("ion.forceinlineCaches", 1);
let offsets = [213, 559, 255, 515, 30, 507, 252, 329, 487, 7];
function update_index(i, j) {
var offset = offsets[j % offsets.length];
return i + offset;
}
function compute_index(initial, count) {
for (var i = 0; i < count; i++) {
initial = update_index(initial, i);
}
return initial;
}
// This is written so that the IC added in the bug activates.
function mutate_array(array, count, epsilon = 0) {
var index = 0;
for (var i = 0; i < count; i++) {
index = update_index(index, i);
array[index] = i + epsilon;
}
return array[offsets[0]+offsets[1]] === (1 + epsilon) &&
array[10] === undefined;
}
// Monomorphizing mutate_array to ensure we get the IC chains we want
function create_variant(variant) {
var source = mutate_array.toString().replace("mutate_array", "mutate_array_"+variant);
return source;
}
function test_basic() {
eval(create_variant("basic"));
var x = [];
var count = 100;
assertEq(mutate_array_basic(x, count), true);
var end = compute_index(0, count);
assertEq(x[end], count - 1);
assertEq(x[end - 1], undefined);
}
// Ensure the IC respects frozen.
function test_frozen() {
eval(create_variant("frozen"));
var x = [];
Object.freeze(x);
var count = 100;
assertEq(mutate_array_frozen(x, count), false);
assertEq(x.length, 0);
var end = compute_index(0, count);
var y = [];
assertEq(mutate_array_frozen(y, count), true);
assertEq(y[end], count - 1);
Object.freeze(y);
// After a mutated array is frozen, can't subsequently modify elements
assertEq(mutate_array_frozen(x, count, 10), false);
assertEq(y[end], count - 1);
}
// Let's make sure updates to the array happen as expected.
function test_update() {
eval(create_variant("update"));
var x = [];
var count = 100;
assertEq(mutate_array_update(x, count), true);
var end = compute_index(0, count);
assertEq(x[end], count - 1);
assertEq(x[end - 1], undefined);
var epsilon = 2;
mutate_array_update(x, 200, epsilon);
assertEq(x[end], count -1 + epsilon)
}
// Elements may be non-writable, let us not write them.
function test_nonwritable() {
eval(create_variant("nonwritable"));
var x = [];
var count = 100;
var index = compute_index(0, 10);
Object.defineProperty(x, index, {value: -10, writable: false});
mutate_array_nonwritable(x, count);
assertEq(x[index], -10);
}
// Random indices can get setters, let's make sure we honour those.
function test_setter() {
eval(create_variant("setter"));
var x = [];
var count = 100;
var index = compute_index(0, 80);
var sigil = 0;
Object.defineProperty(x, index, {set(newVal) {sigil++; }});
mutate_array_setter(x, count);
assertEq(sigil, 1);
assertEq(x[index], undefined);
}
// Ensure indexes on the prototype don't break things;
//
function test_proto_indices() {
eval(create_variant("proto_indices"));
var x = [];
var count = 100;
var index = compute_index(0, 80);
x.__proto__[index] = "hello";
mutate_array_proto_indices(x, count);
assertEq(x.__proto__[index], "hello");
assertEq(x[index], 79);
}
test_basic();
test_frozen();
test_update();
test_nonwritable();
test_setter();
test_proto_indices();

View File

@ -1879,6 +1879,33 @@ BaselineCacheIRCompiler::emitCallProxySetByValue()
return true;
}
bool
BaselineCacheIRCompiler::emitCallAddOrUpdateSparseElementHelper()
{
Register obj = allocator.useRegister(masm, reader.objOperandId());
Register id = allocator.useRegister(masm, reader.int32OperandId());
ValueOperand val = allocator.useValueRegister(masm, reader.valOperandId());
bool strict = reader.readBool();
AutoScratchRegister scratch(allocator, masm);
allocator.discardStack(masm);
AutoStubFrame stubFrame(*this);
stubFrame.enter(masm, scratch);
masm.Push(Imm32(strict));
masm.Push(val);
masm.Push(id);
masm.Push(obj);
if (!callVM(masm, AddOrUpdateSparseElementHelperInfo)) {
return false;
}
stubFrame.leave(masm);
return true;
}
bool
BaselineCacheIRCompiler::emitMegamorphicSetElement()
{

View File

@ -3450,6 +3450,9 @@ SetPropIRGenerator::tryAttachStub()
if (tryAttachSetTypedElement(obj, objId, index, indexId, rhsValId)) {
return true;
}
if (tryAttachAddOrUpdateSparseElement(obj, objId, index, indexId, rhsValId)) {
return true;
}
return false;
}
return false;
@ -4066,6 +4069,89 @@ SetPropIRGenerator::tryAttachSetDenseElementHole(HandleObject obj, ObjOperandId
return true;
}
// Add an IC for adding or updating a sparse array element.
bool
SetPropIRGenerator::tryAttachAddOrUpdateSparseElement(HandleObject obj, ObjOperandId objId,
uint32_t index, Int32OperandId indexId,
ValOperandId rhsId)
{
JSOp op = JSOp(*pc_);
MOZ_ASSERT(IsPropertySetOp(op) || IsPropertyInitOp(op));
if (op != JSOP_SETELEM && op != JSOP_STRICTSETELEM) {
return false;
}
if (!obj->isNative()) {
return false;
}
RootedNativeObject nobj(cx_, &obj->as<NativeObject>());
// We cannot attach a stub to a non-extensible object
if (!nobj->isExtensible()) {
return false;
}
// Stub doesn't handle negative indices.
if (index > INT_MAX) {
return false;
}
// We also need to be past the end of the dense capacity, to ensure sparse.
if (index < nobj->getDenseInitializedLength()) {
return false;
}
// Only handle Array objects in this stub.
if (!nobj->is<ArrayObject>()) {
return false;
}
RootedArrayObject aobj(cx_, &obj->as<ArrayObject>());
// Don't attach if we're adding to an array with non-writable length.
bool isAdd = (index >= aobj->length());
if (isAdd && !aobj->lengthIsWritable()) {
return false;
}
// Indexed properties on the prototype chain aren't handled by the helper.
if (ObjectMayHaveExtraIndexedProperties(aobj->staticPrototype())) {
return false;
}
// Ensure we are still talking about an array class.
writer.guardClass(objId, GuardClassKind::Array);
// The helper we are going to call only applies to non-dense elements.
writer.guardIndexGreaterThanDenseInitLength(objId, indexId);
// Guard extensible: We may be trying to add a new element, and so we'd best
// be able to do so safely.
writer.guardIsExtensible(objId);
// Ensures we are able to efficiently able to map to an integral jsid.
writer.guardIndexIsNonNegative(indexId);
// Shape guard the prototype chain to avoid shadowing indexes from appearing.
// Dense elements may appear on the prototype chain (and prototypes may
// have a different notion of which elements are dense), but they can
// only be data properties, so our specialized Set handler is ok to bind
// to them.
ShapeGuardProtoChain(writer, obj, objId);
// Ensure that if we're adding an element to the object, the object's
// length is writable.
writer.guardIndexIsValidUpdateOrAdd(objId, indexId);
writer.callAddOrUpdateSparseElementHelper(objId, indexId, rhsId,
/* strict = */op == JSOP_STRICTSETELEM);
writer.returnFromIC();
trackAttached("AddOrUpdateSparseElement");
return true;
}
bool
SetPropIRGenerator::tryAttachSetTypedElement(HandleObject obj, ObjOperandId objId,
uint32_t index, Int32OperandId indexId,

View File

@ -201,6 +201,7 @@ extern const char* const CacheKindNames[];
_(GuardClass) /* Guard an object class, per GuardClassKind */ \
_(GuardAnyClass) /* Guard an arbitrary class for an object */ \
_(GuardCompartment) \
_(GuardIsExtensible) \
_(GuardIsNativeFunction) \
_(GuardIsNativeObject) \
_(GuardIsProxy) \
@ -222,6 +223,9 @@ extern const char* const CacheKindNames[];
_(GuardHasGetterSetter) \
_(GuardGroupHasUnanalyzedNewScript) \
_(GuardIndexIsNonNegative) \
_(GuardIndexGreaterThanDenseCapacity) \
_(GuardIndexGreaterThanArrayLength) \
_(GuardIndexIsValidUpdateOrAdd) \
_(GuardIndexGreaterThanDenseInitLength) \
_(GuardTagNotEqual) \
_(GuardXrayExpandoShapeAndDefaultProto) \
@ -267,6 +271,7 @@ extern const char* const CacheKindNames[];
_(CallSetArrayLength) \
_(CallProxySet) \
_(CallProxySetByValue) \
_(CallAddOrUpdateSparseElementHelper) \
_(CallInt32ToString) \
_(CallNumberToString) \
\
@ -765,6 +770,9 @@ class MOZ_RAII CacheIRWriter : public JS::CustomAutoRooter
// Use RawWord, because compartments never move and it can't be GCed.
addStubField(uintptr_t(compartment), StubField::Type::RawWord);
}
void guardIsExtensible(ObjOperandId obj) {
writeOpWithOperandId(CacheOp::GuardIsExtensible, obj);
}
void guardNoDetachedTypedObjects() {
writeOp(CacheOp::GuardNoDetachedTypedObjects);
}
@ -811,6 +819,18 @@ class MOZ_RAII CacheIRWriter : public JS::CustomAutoRooter
writeOpWithOperandId(CacheOp::GuardIndexGreaterThanDenseInitLength, obj);
writeOperandId(index);
}
void guardIndexGreaterThanDenseCapacity(ObjOperandId obj, Int32OperandId index) {
writeOpWithOperandId(CacheOp::GuardIndexGreaterThanDenseCapacity, obj);
writeOperandId(index);
}
void guardIndexGreaterThanArrayLength(ObjOperandId obj, Int32OperandId index) {
writeOpWithOperandId(CacheOp::GuardIndexGreaterThanArrayLength, obj);
writeOperandId(index);
}
void guardIndexIsValidUpdateOrAdd(ObjOperandId obj, Int32OperandId index) {
writeOpWithOperandId(CacheOp::GuardIndexIsValidUpdateOrAdd, obj);
writeOperandId(index);
}
void guardTagNotEqual(ValueTagOperandId lhs, ValueTagOperandId rhs) {
writeOpWithOperandId(CacheOp::GuardTagNotEqual, lhs);
writeOperandId(rhs);
@ -1041,6 +1061,12 @@ class MOZ_RAII CacheIRWriter : public JS::CustomAutoRooter
writeOperandId(rhs);
buffer_.writeByte(uint32_t(strict));
}
void callAddOrUpdateSparseElementHelper(ObjOperandId obj, Int32OperandId id, ValOperandId rhs, bool strict) {
writeOpWithOperandId(CacheOp::CallAddOrUpdateSparseElementHelper, obj);
writeOperandId(id);
writeOperandId(rhs);
buffer_.writeByte(uint32_t(strict));
}
StringOperandId callInt32ToString(Int32OperandId id) {
StringOperandId res(nextOperandId_++);
writeOpWithOperandId(CacheOp::CallInt32ToString, id);
@ -1753,6 +1779,10 @@ class MOZ_RAII SetPropIRGenerator : public IRGenerator
bool tryAttachSetDenseElementHole(HandleObject obj, ObjOperandId objId, uint32_t index,
Int32OperandId indexId, ValOperandId rhsId);
bool tryAttachAddOrUpdateSparseElement(HandleObject obj, ObjOperandId objId, uint32_t index,
Int32OperandId indexId, ValOperandId rhsId);
bool tryAttachGenericProxy(HandleObject obj, ObjOperandId objId, HandleId id,
ValOperandId rhsId, bool handleDOMProxies);
bool tryAttachDOMProxyShadowed(HandleObject obj, ObjOperandId objId, HandleId id,

View File

@ -1734,6 +1734,35 @@ CacheIRCompiler::emitGuardClass()
return true;
}
bool
CacheIRCompiler::emitGuardIsExtensible()
{
Register obj = allocator.useRegister(masm, reader.objOperandId());
AutoScratchRegister scratch(allocator, masm);
FailurePath* failure;
if (!addFailurePath(&failure)) {
return false;
}
Address shape(obj, ShapedObject::offsetOfShape());
masm.loadPtr(shape, scratch);
Address baseShape(scratch, Shape::offsetOfBaseShape());
masm.loadPtr(baseShape, scratch);
Address baseShapeFlags(scratch, BaseShape::offsetOfFlags());
masm.loadPtr(baseShapeFlags, scratch);
masm.and32(Imm32(js::BaseShape::NOT_EXTENSIBLE), scratch);
// Spectre-style checks are not needed here because we do not
// interpret data based on this check.
masm.branch32(Assembler::Equal, scratch, Imm32(js::BaseShape::NOT_EXTENSIBLE),
failure->label());
return true;
}
bool
CacheIRCompiler::emitGuardIsNativeFunction()
{
@ -2836,7 +2865,7 @@ CacheIRCompiler::emitGuardIndexGreaterThanDenseInitLength()
// Load obj->elements.
masm.loadPtr(Address(obj, NativeObject::offsetOfElements()), scratch);
// Ensure index >= capacity.
// Ensure index >= initLength.
Label outOfBounds;
Address capacity(scratch, ObjectElements::offsetOfInitializedLength());
masm.spectreBoundsCheck32(index, capacity, scratch2, &outOfBounds);
@ -2846,6 +2875,89 @@ CacheIRCompiler::emitGuardIndexGreaterThanDenseInitLength()
return true;
}
bool
CacheIRCompiler::emitGuardIndexGreaterThanDenseCapacity()
{
Register obj = allocator.useRegister(masm, reader.objOperandId());
Register index = allocator.useRegister(masm, reader.int32OperandId());
AutoScratchRegister scratch(allocator, masm);
AutoScratchRegister scratch2(allocator, masm);
FailurePath* failure;
if (!addFailurePath(&failure)) {
return false;
}
// Load obj->elements.
masm.loadPtr(Address(obj, NativeObject::offsetOfElements()), scratch);
// Ensure index >= capacity.
Label outOfBounds;
Address capacity(scratch, ObjectElements::offsetOfCapacity());
masm.spectreBoundsCheck32(index, capacity, scratch2, &outOfBounds);
masm.jump(failure->label());
masm.bind(&outOfBounds);
return true;
}
bool
CacheIRCompiler::emitGuardIndexGreaterThanArrayLength()
{
Register obj = allocator.useRegister(masm, reader.objOperandId());
Register index = allocator.useRegister(masm, reader.int32OperandId());
AutoScratchRegister scratch(allocator, masm);
AutoScratchRegister scratch2(allocator, masm);
FailurePath* failure;
if (!addFailurePath(&failure)) {
return false;
}
// Load obj->elements.
masm.loadPtr(Address(obj, NativeObject::offsetOfElements()), scratch);
// Ensure index >= length;
Label outOfBounds;
Address length(scratch, ObjectElements::offsetOfLength());
masm.spectreBoundsCheck32(index, length, scratch2, &outOfBounds);
masm.jump(failure->label());
masm.bind(&outOfBounds);
return true;
}
bool
CacheIRCompiler::emitGuardIndexIsValidUpdateOrAdd()
{
Register obj = allocator.useRegister(masm, reader.objOperandId());
Register index = allocator.useRegister(masm, reader.int32OperandId());
AutoScratchRegister scratch(allocator, masm);
AutoScratchRegister scratch2(allocator, masm);
FailurePath* failure;
if (!addFailurePath(&failure)) {
return false;
}
// Load obj->elements.
masm.loadPtr(Address(obj, NativeObject::offsetOfElements()), scratch);
Label success;
// If length is writable, branch to &success. All indices are writable.
Address flags(scratch, ObjectElements::offsetOfFlags());
masm.branchTest32(Assembler::Zero, flags,
Imm32(ObjectElements::Flags::NONWRITABLE_ARRAY_LENGTH),
&success);
// Otherwise, ensure index is in bounds.
Address length(scratch, ObjectElements::offsetOfLength());
masm.spectreBoundsCheck32(index, length, scratch2,
/* failure = */ failure->label());
masm.bind(&success);
return true;
}
bool
CacheIRCompiler::emitGuardTagNotEqual()
{

View File

@ -32,6 +32,7 @@ namespace jit {
_(GuardType) \
_(GuardClass) \
_(GuardGroupHasUnanalyzedNewScript) \
_(GuardIsExtensible) \
_(GuardIsNativeFunction) \
_(GuardFunctionPrototype) \
_(GuardIsNativeObject) \
@ -46,6 +47,9 @@ namespace jit {
_(GuardAndGetNumberFromString) \
_(GuardAndGetIndexFromString) \
_(GuardIndexIsNonNegative) \
_(GuardIndexGreaterThanDenseCapacity) \
_(GuardIndexGreaterThanArrayLength) \
_(GuardIndexIsValidUpdateOrAdd) \
_(GuardIndexGreaterThanDenseInitLength) \
_(GuardTagNotEqual) \
_(GuardXrayExpandoShapeAndDefaultProto)\

View File

@ -2265,6 +2265,28 @@ IonCacheIRCompiler::emitCallProxySetByValue()
return callVM(masm, ProxySetPropertyByValueInfo);
}
bool
IonCacheIRCompiler::emitCallAddOrUpdateSparseElementHelper()
{
AutoSaveLiveRegisters save(*this);
Register obj = allocator.useRegister(masm, reader.objOperandId());
Register id = allocator.useRegister(masm, reader.int32OperandId());
ValueOperand val = allocator.useValueRegister(masm, reader.valOperandId());
bool strict = reader.readBool();
Label done;
prepareVMCall(masm, save);
masm.Push(Imm32(strict));
masm.Push(val);
masm.Push(id);
masm.Push(obj);
return callVM(masm, AddOrUpdateSparseElementHelperInfo);
}
bool
IonCacheIRCompiler::emitMegamorphicSetElement()
{

View File

@ -2072,5 +2072,10 @@ typedef bool (*NativeGetElementFn)(JSContext*, HandleNativeObject, HandleValue,
const VMFunction NativeGetElementInfo =
FunctionInfo<NativeGetElementFn>(NativeGetElement, "NativeGetProperty");
typedef bool (*AddOrUpdateSparseElementHelperFn)(JSContext* cx, HandleArrayObject obj,
int32_t int_id, HandleValue v, bool strict);
const VMFunction AddOrUpdateSparseElementHelperInfo =
FunctionInfo<AddOrUpdateSparseElementHelperFn>(AddOrUpdateSparseElementHelper, "AddOrUpdateSparseElementHelper");
} // namespace jit
} // namespace js

View File

@ -982,6 +982,8 @@ extern const VMFunction ProxyHasOwnInfo;
extern const VMFunction NativeGetElementInfo;
extern const VMFunction AddOrUpdateSparseElementHelperInfo;
// TailCall VMFunctions
extern const VMFunction DoConcatStringObjectInfo;

View File

@ -2107,6 +2107,48 @@ DefineNonexistentProperty(JSContext* cx, HandleNativeObject obj, HandleId id,
return result.succeed();
}
bool
js::AddOrUpdateSparseElementHelper(JSContext* cx, HandleArrayObject obj, int32_t int_id,
HandleValue v, bool strict)
{
MOZ_ASSERT(INT_FITS_IN_JSID(int_id));
RootedId id(cx, INT_TO_JSID(int_id));
// This helper doesn't handle the case where the index may be in the dense elements
MOZ_ASSERT(int_id >= 0);
MOZ_ASSERT(uint32_t(int_id) >= obj->getDenseInitializedLength());
// First decide if this is an add or an update. Because the IC guards have
// already ensured this exists exterior to the dense array range, and the
// prototype checks have ensured there are no indexes on the prototype, we
// can use the shape lineage to find the element if it exists:
RootedShape shape(cx, obj->lastProperty()->search(cx, id));
// If we didn't find the shape, we're on the add path: delegate to
// AddSparseElement:
if (shape == nullptr) {
Rooted<PropertyDescriptor> desc(cx);
desc.setDataDescriptor(v, JSPROP_ENUMERATE);
desc.assertComplete();
return AddOrChangeProperty<IsAddOrChange::Add>(cx, obj, id, desc);
}
// At this point we're updating a property: See SetExistingProperty
if (shape->writable() && shape->isDataProperty()) {
// While all JSID_INT properties use a single TI entry,
// nothing yet has inspected the updated value so we *must* use setSlotWithType().
obj->setSlotWithType(cx, shape, v, /* overwriting = */ true);
return true;
}
// We don't know exactly what this object looks like, hit the slowpath.
RootedValue receiver(cx, ObjectValue(*obj));
JS::ObjectOpResult result;
return SetProperty(cx, obj, id, v, receiver, result) &&
result.checkStrictErrorOrWarning(cx, obj, id, strict);
}
/*** [[HasProperty]] *****************************************************************************/

View File

@ -1622,6 +1622,10 @@ bool
SetPropertyOnProto(JSContext* cx, HandleObject obj, HandleId id, HandleValue v,
HandleValue receiver, ObjectOpResult& result);
bool
AddOrUpdateSparseElementHelper(JSContext* cx, HandleArrayObject obj, int32_t int_id,
HandleValue v, bool strict);
/*
* Indicates whether an assignment operation is qualified (`x.y = 0`) or
* unqualified (`y = 0`). In strict mode, the latter is an error if no such

View File

@ -1159,8 +1159,10 @@ class Shape : public gc::TenuredCell
void fixupGetterSetterForBarrier(JSTracer* trc);
void updateBaseShapeAfterMovingGC();
#ifdef DEBUG
// For JIT usage.
static inline size_t offsetOfBaseShape() { return offsetof(Shape, base_); }
#ifdef DEBUG
static inline size_t offsetOfImmutableFlags() { return offsetof(Shape, immutableFlags); }
static inline uint32_t fixedSlotsMask() { return FIXED_SLOTS_MASK; }
#endif

View File

@ -11,6 +11,8 @@
namespace js {
namespace jit { class CacheIRCompiler; }
/*
* Shaped objects are a variant of JSObject that use a GCPtrShape for their
* |shapeOrExpando_| field. All objects that point to a js::Shape as their
@ -58,6 +60,8 @@ class ShapedObject : public JSObject
// See JSObject::offsetOfGroup() comment.
friend class js::jit::MacroAssembler;
friend class js::jit::CacheIRCompiler;
static constexpr size_t offsetOfShape() {
static_assert(offsetOfShapeOrExpando() == offsetof(shadow::Object, shape),
"shadow shape must match actual shape");