Implement immutable prototype chains
This patch implements "immutable prototype exotic objects" from the ECMAScript spec, which are objects whose __proto__ cannot be changed, but are not otherwise frozen. They are introduced in order to prevent a Proxy from being introduced to the prototype chain of the global object. The API is extended by a SetImmutablePrototype() call in ObjectTemplate, which can be used to vend new immutable prototype objects. Additionally, Object.prototype is an immutable prototype object. In the implementation, a new bit is added to Maps to say whether the prototype is immutable, which is read by SetPrototype. Map transitions to the immutable prototype state are not saved in the transition tree because the main use case is just for the prototype chain of the global object, which there will be only one of per Context, so no need to take up the extra word for a pointer in each full transition tree. BUG=v8:5149 Review-Url: https://codereview.chromium.org/2108203002 Cr-Commit-Position: refs/heads/master@{#37482}
This commit is contained in:
parent
e65035f257
commit
0ff7b4830c
11
include/v8.h
11
include/v8.h
@ -4833,6 +4833,17 @@ class V8_EXPORT ObjectTemplate : public Template {
|
||||
*/
|
||||
void SetInternalFieldCount(int value);
|
||||
|
||||
/**
|
||||
* Returns true if the object will be an immutable prototype exotic object.
|
||||
*/
|
||||
bool IsImmutableProto();
|
||||
|
||||
/**
|
||||
* Makes the ObjectTempate for an immutable prototype exotic object, with an
|
||||
* immutable __proto__.
|
||||
*/
|
||||
void SetImmutableProto();
|
||||
|
||||
private:
|
||||
ObjectTemplate();
|
||||
static Local<ObjectTemplate> New(internal::Isolate* isolate,
|
||||
|
@ -304,7 +304,8 @@ MaybeHandle<JSObject> InstantiateObject(Isolate* isolate,
|
||||
JSFunction::cast(*new_target)->shared()->function_data() ==
|
||||
info->constructor() &&
|
||||
JSFunction::cast(*new_target)->context()->native_context() ==
|
||||
isolate->context()->native_context()) {
|
||||
isolate->context()->native_context() &&
|
||||
!info->immutable_proto()) {
|
||||
constructor = Handle<JSFunction>::cast(new_target);
|
||||
} else {
|
||||
// Disable caching for subclass instantiation.
|
||||
@ -346,6 +347,9 @@ MaybeHandle<JSObject> InstantiateObject(Isolate* isolate,
|
||||
ASSIGN_RETURN_ON_EXCEPTION(
|
||||
isolate, result,
|
||||
ConfigureInstance(isolate, object, info, is_hidden_prototype), JSObject);
|
||||
if (info->immutable_proto()) {
|
||||
JSObject::SetImmutableProto(object);
|
||||
}
|
||||
// TODO(dcarney): is this necessary?
|
||||
JSObject::MigrateSlowToFast(result, 0, "ApiNatives::InstantiateObject");
|
||||
|
||||
@ -572,8 +576,7 @@ Handle<JSFunction> ApiNatives::CreateApiFunction(
|
||||
if (!obj->instance_template()->IsUndefined(isolate)) {
|
||||
Handle<ObjectTemplateInfo> instance_template = Handle<ObjectTemplateInfo>(
|
||||
ObjectTemplateInfo::cast(obj->instance_template()));
|
||||
internal_field_count =
|
||||
Smi::cast(instance_template->internal_field_count())->value();
|
||||
internal_field_count = instance_template->internal_field_count();
|
||||
}
|
||||
|
||||
// TODO(svenpanne) Kill ApiInstanceType and refactor things by generalizing
|
||||
|
15
src/api.cc
15
src/api.cc
@ -1417,7 +1417,7 @@ static Local<ObjectTemplate> ObjectTemplateNew(
|
||||
obj->set_serial_number(i::Smi::FromInt(next_serial_number));
|
||||
if (!constructor.IsEmpty())
|
||||
obj->set_constructor(*Utils::OpenHandle(*constructor));
|
||||
obj->set_internal_field_count(i::Smi::FromInt(0));
|
||||
obj->set_data(i::Smi::FromInt(0));
|
||||
return Utils::ToLocal(obj);
|
||||
}
|
||||
|
||||
@ -1712,7 +1712,7 @@ void ObjectTemplate::SetCallAsFunctionHandler(FunctionCallback callback,
|
||||
|
||||
|
||||
int ObjectTemplate::InternalFieldCount() {
|
||||
return i::Smi::cast(Utils::OpenHandle(this)->internal_field_count())->value();
|
||||
return Utils::OpenHandle(this)->internal_field_count();
|
||||
}
|
||||
|
||||
|
||||
@ -1730,9 +1730,18 @@ void ObjectTemplate::SetInternalFieldCount(int value) {
|
||||
// function to do the setting.
|
||||
EnsureConstructor(isolate, this);
|
||||
}
|
||||
Utils::OpenHandle(this)->set_internal_field_count(i::Smi::FromInt(value));
|
||||
Utils::OpenHandle(this)->set_internal_field_count(value);
|
||||
}
|
||||
|
||||
bool ObjectTemplate::IsImmutableProto() {
|
||||
return Utils::OpenHandle(this)->immutable_proto();
|
||||
}
|
||||
|
||||
void ObjectTemplate::SetImmutableProto() {
|
||||
i::Isolate* isolate = Utils::OpenHandle(this)->GetIsolate();
|
||||
ENTER_V8(isolate);
|
||||
Utils::OpenHandle(this)->set_immutable_proto(true);
|
||||
}
|
||||
|
||||
// --- S c r i p t s ---
|
||||
|
||||
|
@ -599,6 +599,8 @@ Handle<JSFunction> Genesis::CreateEmptyFunction(Isolate* isolate) {
|
||||
Handle<Map> map = Map::Copy(handle(object_function_prototype->map()),
|
||||
"EmptyObjectPrototype");
|
||||
map->set_is_prototype_map(true);
|
||||
// Ban re-setting Object.prototype.__proto__ to prevent Proxy security bug
|
||||
map->set_immutable_proto(true);
|
||||
object_function_prototype->set_map(*map);
|
||||
|
||||
native_context()->set_initial_object_prototype(*object_function_prototype);
|
||||
|
@ -128,6 +128,8 @@ class CallSite {
|
||||
T(FunctionBind, "Bind must be called on a function") \
|
||||
T(GeneratorRunning, "Generator is already running") \
|
||||
T(IllegalInvocation, "Illegal invocation") \
|
||||
T(ImmutablePrototypeSet, \
|
||||
"Immutable prototype object '%' cannot have their prototype set") \
|
||||
T(IncompatibleMethodReceiver, "Method % called on incompatible receiver %") \
|
||||
T(InstanceofNonobjectProto, \
|
||||
"Function has non-object prototype '%' in instanceof check") \
|
||||
|
@ -991,7 +991,7 @@ void ObjectTemplateInfo::ObjectTemplateInfoVerify() {
|
||||
CHECK(IsObjectTemplateInfo());
|
||||
TemplateInfoVerify();
|
||||
VerifyPointer(constructor());
|
||||
VerifyPointer(internal_field_count());
|
||||
VerifyPointer(data());
|
||||
}
|
||||
|
||||
|
||||
|
@ -4725,6 +4725,13 @@ bool Map::is_migration_target() {
|
||||
return IsMigrationTarget::decode(bit_field3());
|
||||
}
|
||||
|
||||
void Map::set_immutable_proto(bool value) {
|
||||
set_bit_field3(ImmutablePrototype::update(bit_field3(), value));
|
||||
}
|
||||
|
||||
bool Map::is_immutable_proto() {
|
||||
return ImmutablePrototype::decode(bit_field3());
|
||||
}
|
||||
|
||||
void Map::set_new_target_is_base(bool value) {
|
||||
set_bit_field3(NewTargetIsBase::update(bit_field3(), value));
|
||||
@ -5639,8 +5646,25 @@ ACCESSORS(FunctionTemplateInfo, shared_function_info, Object,
|
||||
SMI_ACCESSORS(FunctionTemplateInfo, flag, kFlagOffset)
|
||||
|
||||
ACCESSORS(ObjectTemplateInfo, constructor, Object, kConstructorOffset)
|
||||
ACCESSORS(ObjectTemplateInfo, internal_field_count, Object,
|
||||
kInternalFieldCountOffset)
|
||||
ACCESSORS(ObjectTemplateInfo, data, Object, kDataOffset)
|
||||
int ObjectTemplateInfo::internal_field_count() const {
|
||||
Object* value = data();
|
||||
DCHECK(value->IsSmi());
|
||||
return InternalFieldCount::decode(Smi::cast(value)->value());
|
||||
}
|
||||
void ObjectTemplateInfo::set_internal_field_count(int count) {
|
||||
return set_data(Smi::FromInt(
|
||||
InternalFieldCount::update(Smi::cast(data())->value(), count)));
|
||||
}
|
||||
bool ObjectTemplateInfo::immutable_proto() const {
|
||||
Object* value = data();
|
||||
DCHECK(value->IsSmi());
|
||||
return IsImmutablePrototype::decode(Smi::cast(value)->value());
|
||||
}
|
||||
void ObjectTemplateInfo::set_immutable_proto(bool immutable) {
|
||||
return set_data(Smi::FromInt(
|
||||
IsImmutablePrototype::update(Smi::cast(data())->value(), immutable)));
|
||||
}
|
||||
|
||||
ACCESSORS(AllocationSite, transition_info, Object, kTransitionInfoOffset)
|
||||
ACCESSORS(AllocationSite, nested_site, Object, kNestedSiteOffset)
|
||||
|
@ -1133,7 +1133,8 @@ void ObjectTemplateInfo::ObjectTemplateInfoPrint(std::ostream& os) { // NOLINT
|
||||
os << "\n - property_list: " << Brief(property_list());
|
||||
os << "\n - property_accessors: " << Brief(property_accessors());
|
||||
os << "\n - constructor: " << Brief(constructor());
|
||||
os << "\n - internal_field_count: " << Brief(internal_field_count());
|
||||
os << "\n - internal_field_count: " << internal_field_count();
|
||||
os << "\n - immutable_proto: " << (immutable_proto() ? "true" : "false");
|
||||
os << "\n";
|
||||
}
|
||||
|
||||
|
@ -8682,6 +8682,17 @@ Handle<Map> Map::CopyNormalized(Handle<Map> map,
|
||||
return result;
|
||||
}
|
||||
|
||||
// Return an immutable prototype exotic object version of the input map.
|
||||
// Never even try to cache it in the transition tree, as it is intended
|
||||
// for the global object and its prototype chain, and excluding it saves
|
||||
// memory on the map transition tree.
|
||||
|
||||
// static
|
||||
Handle<Map> Map::TransitionToImmutableProto(Handle<Map> map) {
|
||||
Handle<Map> new_map = Map::Copy(map, "ImmutablePrototype");
|
||||
new_map->set_immutable_proto(true);
|
||||
return new_map;
|
||||
}
|
||||
|
||||
Handle<Map> Map::CopyInitialMap(Handle<Map> map, int instance_size,
|
||||
int in_object_properties,
|
||||
@ -14920,6 +14931,13 @@ Maybe<bool> JSObject::SetPrototype(Handle<JSObject> object,
|
||||
// Nothing to do if prototype is already set.
|
||||
if (map->prototype() == *value) return Just(true);
|
||||
|
||||
bool immutable_proto = object->map()->is_immutable_proto();
|
||||
if (immutable_proto) {
|
||||
RETURN_FAILURE(
|
||||
isolate, should_throw,
|
||||
NewTypeError(MessageTemplate::kImmutablePrototypeSet, object));
|
||||
}
|
||||
|
||||
// From 8.6.2 Object Internal Methods
|
||||
// ...
|
||||
// In addition, if [[Extensible]] is false the value of the [[Class]] and
|
||||
@ -14971,6 +14989,17 @@ Maybe<bool> JSObject::SetPrototype(Handle<JSObject> object,
|
||||
return Just(true);
|
||||
}
|
||||
|
||||
// static
|
||||
void JSObject::SetImmutableProto(Handle<JSObject> object) {
|
||||
DCHECK(!object->IsAccessCheckNeeded()); // Never called from JS
|
||||
Handle<Map> map(object->map());
|
||||
|
||||
// Nothing to do if prototype is already set.
|
||||
if (map->is_immutable_proto()) return;
|
||||
|
||||
Handle<Map> new_map = Map::TransitionToImmutableProto(map);
|
||||
object->set_map(*new_map);
|
||||
}
|
||||
|
||||
void JSObject::EnsureCanContainElements(Handle<JSObject> object,
|
||||
Arguments* args,
|
||||
|
@ -2346,6 +2346,10 @@ class JSObject: public JSReceiver {
|
||||
bool from_javascript,
|
||||
ShouldThrow should_throw);
|
||||
|
||||
// Makes the object prototype immutable
|
||||
// Never called from JavaScript
|
||||
static void SetImmutableProto(Handle<JSObject> object);
|
||||
|
||||
// Initializes the body starting at |start_offset|. It is responsibility of
|
||||
// the caller to initialize object header. Fill the pre-allocated fields with
|
||||
// pre_allocated_value and the rest with filler_value.
|
||||
@ -5702,7 +5706,7 @@ class Map: public HeapObject {
|
||||
class Deprecated : public BitField<bool, 23, 1> {};
|
||||
class IsUnstable : public BitField<bool, 24, 1> {};
|
||||
class IsMigrationTarget : public BitField<bool, 25, 1> {};
|
||||
// Bit 26 is free.
|
||||
class ImmutablePrototype : public BitField<bool, 26, 1> {};
|
||||
class NewTargetIsBase : public BitField<bool, 27, 1> {};
|
||||
// Bit 28 is free.
|
||||
|
||||
@ -5990,6 +5994,8 @@ class Map: public HeapObject {
|
||||
inline bool is_stable();
|
||||
inline void set_migration_target(bool value);
|
||||
inline bool is_migration_target();
|
||||
inline void set_immutable_proto(bool value);
|
||||
inline bool is_immutable_proto();
|
||||
inline void set_construction_counter(int value);
|
||||
inline int construction_counter();
|
||||
inline void deprecate();
|
||||
@ -6162,6 +6168,8 @@ class Map: public HeapObject {
|
||||
Handle<Object> prototype,
|
||||
PrototypeOptimizationMode mode);
|
||||
|
||||
static Handle<Map> TransitionToImmutableProto(Handle<Map> map);
|
||||
|
||||
static const int kMaxPreAllocatedPropertyFields = 255;
|
||||
|
||||
// Layout description.
|
||||
@ -10716,7 +10724,9 @@ class FunctionTemplateInfo: public TemplateInfo {
|
||||
class ObjectTemplateInfo: public TemplateInfo {
|
||||
public:
|
||||
DECL_ACCESSORS(constructor, Object)
|
||||
DECL_ACCESSORS(internal_field_count, Object)
|
||||
DECL_ACCESSORS(data, Object)
|
||||
DECL_INT_ACCESSORS(internal_field_count)
|
||||
DECL_BOOLEAN_ACCESSORS(immutable_proto)
|
||||
|
||||
DECLARE_CAST(ObjectTemplateInfo)
|
||||
|
||||
@ -10725,9 +10735,14 @@ class ObjectTemplateInfo: public TemplateInfo {
|
||||
DECLARE_VERIFIER(ObjectTemplateInfo)
|
||||
|
||||
static const int kConstructorOffset = TemplateInfo::kHeaderSize;
|
||||
static const int kInternalFieldCountOffset =
|
||||
kConstructorOffset + kPointerSize;
|
||||
static const int kSize = kInternalFieldCountOffset + kPointerSize;
|
||||
// LSB is for immutable_proto, higher bits for internal_field_count
|
||||
static const int kDataOffset = kConstructorOffset + kPointerSize;
|
||||
static const int kSize = kDataOffset + kPointerSize;
|
||||
|
||||
private:
|
||||
class IsImmutablePrototype : public BitField<bool, 0, 1> {};
|
||||
class InternalFieldCount
|
||||
: public BitField<int, IsImmutablePrototype::kNext, 29> {};
|
||||
};
|
||||
|
||||
|
||||
|
@ -91,7 +91,7 @@ bytecodes: [
|
||||
B(TestEqualStrict), R(12),
|
||||
B(JumpIfFalse), U8(4),
|
||||
B(Jump), U8(18),
|
||||
B(Wide), B(LdaSmi), U16(128),
|
||||
B(Wide), B(LdaSmi), U16(129),
|
||||
B(Star), R(12),
|
||||
B(LdaConstant), U8(8),
|
||||
B(Star), R(13),
|
||||
@ -235,7 +235,7 @@ bytecodes: [
|
||||
B(TestEqualStrict), R(13),
|
||||
B(JumpIfFalse), U8(4),
|
||||
B(Jump), U8(18),
|
||||
B(Wide), B(LdaSmi), U16(128),
|
||||
B(Wide), B(LdaSmi), U16(129),
|
||||
B(Star), R(13),
|
||||
B(LdaConstant), U8(8),
|
||||
B(Star), R(14),
|
||||
@ -392,7 +392,7 @@ bytecodes: [
|
||||
B(TestEqualStrict), R(12),
|
||||
B(JumpIfFalse), U8(4),
|
||||
B(Jump), U8(18),
|
||||
B(Wide), B(LdaSmi), U16(128),
|
||||
B(Wide), B(LdaSmi), U16(129),
|
||||
B(Star), R(12),
|
||||
B(LdaConstant), U8(8),
|
||||
B(Star), R(13),
|
||||
@ -539,7 +539,7 @@ bytecodes: [
|
||||
B(TestEqualStrict), R(11),
|
||||
B(JumpIfFalse), U8(4),
|
||||
B(Jump), U8(18),
|
||||
B(Wide), B(LdaSmi), U16(128),
|
||||
B(Wide), B(LdaSmi), U16(129),
|
||||
B(Star), R(11),
|
||||
B(LdaConstant), U8(10),
|
||||
B(Star), R(12),
|
||||
|
@ -480,7 +480,7 @@ bytecodes: [
|
||||
B(TestEqualStrict), R(10),
|
||||
B(JumpIfFalse), U8(4),
|
||||
B(Jump), U8(18),
|
||||
B(Wide), B(LdaSmi), U16(128),
|
||||
B(Wide), B(LdaSmi), U16(129),
|
||||
B(Star), R(10),
|
||||
B(LdaConstant), U8(14),
|
||||
B(Star), R(11),
|
||||
|
@ -25348,3 +25348,32 @@ TEST(PrivateForApiIsNumber) {
|
||||
// Shouldn't crash.
|
||||
v8::Private::ForApi(isolate, v8_str("42"));
|
||||
}
|
||||
|
||||
THREADED_TEST(ImmutableProto) {
|
||||
LocalContext context;
|
||||
v8::Isolate* isolate = context->GetIsolate();
|
||||
v8::HandleScope handle_scope(isolate);
|
||||
|
||||
Local<v8::FunctionTemplate> templ = v8::FunctionTemplate::New(isolate);
|
||||
templ->InstanceTemplate()->SetImmutableProto();
|
||||
|
||||
Local<v8::Object> object = templ->GetFunction(context.local())
|
||||
.ToLocalChecked()
|
||||
->NewInstance(context.local())
|
||||
.ToLocalChecked();
|
||||
|
||||
// Look up the prototype
|
||||
Local<v8::Value> original_proto =
|
||||
object->Get(context.local(), v8_str("__proto__")).ToLocalChecked();
|
||||
|
||||
// Setting the prototype (e.g., to null) throws
|
||||
CHECK(object->SetPrototype(context.local(), v8::Null(isolate)).IsNothing());
|
||||
|
||||
// The original prototype is still there
|
||||
Local<Value> new_proto =
|
||||
object->Get(context.local(), v8_str("__proto__")).ToLocalChecked();
|
||||
CHECK(new_proto->IsObject());
|
||||
CHECK(new_proto.As<v8::Object>()
|
||||
->Equals(context.local(), original_proto)
|
||||
.FromJust());
|
||||
}
|
||||
|
@ -5,8 +5,5 @@
|
||||
var failing_proxy = new Proxy({}, new Proxy({}, {
|
||||
get() { throw "No trap should fire" }}));
|
||||
|
||||
Object.setPrototypeOf(Object.prototype, failing_proxy);
|
||||
assertThrows(()=>a, TypeError);
|
||||
|
||||
Object.setPrototypeOf(this, failing_proxy);
|
||||
assertThrows(()=>a, TypeError);
|
||||
assertThrows(() => Object.setPrototypeOf(Object.prototype, failing_proxy), TypeError);
|
||||
assertThrows(()=>a, ReferenceError);
|
||||
|
@ -28,7 +28,7 @@
|
||||
// See: http://code.google.com/p/v8/issues/detail?id=1403
|
||||
|
||||
a = [];
|
||||
Object.prototype.__proto__ = { __proto__: null };
|
||||
assertThrows(() => Object.prototype.__proto__ = { __proto__: null }, TypeError);
|
||||
a.shift();
|
||||
|
||||
a = [];
|
||||
|
@ -11,7 +11,10 @@ function f(a) {
|
||||
|
||||
var rec = new Receiver();
|
||||
|
||||
var proto = rec.__proto__.__proto__;
|
||||
// Formerly, this mutated rec.__proto__.__proto__, but
|
||||
// the global object prototype chain is now immutable;
|
||||
// not sure if this test now hits the original hazard case.
|
||||
var proto = rec.__proto__;
|
||||
|
||||
// Initialize prototype chain dependent IC (nonexistent load).
|
||||
assertEquals(undefined, f(rec));
|
||||
|
Loading…
Reference in New Issue
Block a user