[classes] Implement private brand checks

Notes: https://docs.google.com/document/d/1fEumNPCcOn4X0N5jGlAT7GQ5CEKKnw0YxLPXMoaSK5Q/edit?usp=sharing

Bug: v8:11374
Change-Id: I96720c0d69fe28e7229c4c22ed3d291587b73f59
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2667511
Commit-Queue: Marja Hölttä <marja@chromium.org>
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
Reviewed-by: Igor Sheludko <ishell@chromium.org>
Reviewed-by: Shu-yu Guo <syg@chromium.org>
Cr-Commit-Position: refs/heads/master@{#72659}
This commit is contained in:
Marja Hölttä 2021-02-11 14:50:07 +01:00 committed by Commit Bot
parent 921a4a7293
commit 1105d7ba5f
9 changed files with 665 additions and 6 deletions

View File

@ -9267,7 +9267,7 @@ void CodeStubAssembler::TryPrototypeChainLookup(
TNode<Object> receiver, TNode<Object> object_arg, TNode<Object> key,
const LookupPropertyInHolder& lookup_property_in_holder,
const LookupElementInHolder& lookup_element_in_holder, Label* if_end,
Label* if_bailout, Label* if_proxy) {
Label* if_bailout, Label* if_proxy, bool handle_private_names) {
// Ensure receiver is JSReceiver, otherwise bailout.
GotoIf(TaggedIsSmi(receiver), if_bailout);
TNode<HeapObject> object = CAST(object_arg);
@ -9322,6 +9322,11 @@ void CodeStubAssembler::TryPrototypeChainLookup(
BIND(&next_proto);
if (handle_private_names) {
// Private name lookup doesn't walk the prototype chain.
GotoIf(IsPrivateSymbol(CAST(key)), if_end);
}
TNode<HeapObject> proto = LoadMapPrototype(holder_map);
GotoIf(IsNull(proto), if_end);
@ -12392,9 +12397,10 @@ TNode<Oddball> CodeStubAssembler::HasProperty(TNode<Context> context,
&return_true, &return_false, next_holder, if_bailout);
};
const bool kHandlePrivateNames = mode == HasPropertyLookupMode::kHasProperty;
TryPrototypeChainLookup(object, object, key, lookup_property_in_holder,
lookup_element_in_holder, &return_false,
&call_runtime, &if_proxy);
&call_runtime, &if_proxy, kHandlePrivateNames);
TVARIABLE(Oddball, result);

View File

@ -3128,7 +3128,7 @@ class V8_EXPORT_PRIVATE CodeStubAssembler
TNode<Object> receiver, TNode<Object> object, TNode<Object> key,
const LookupPropertyInHolder& lookup_property_in_holder,
const LookupElementInHolder& lookup_element_in_holder, Label* if_end,
Label* if_bailout, Label* if_proxy);
Label* if_bailout, Label* if_proxy, bool handle_private_names = false);
// Instanceof helpers.
// Returns true if {object} has {prototype} somewhere in it's prototype

View File

@ -249,7 +249,8 @@ DEFINE_IMPLICATION(harmony_weak_refs_with_cleanup_some, harmony_weak_refs)
V(harmony_regexp_sequence, "RegExp Unicode sequence properties") \
V(harmony_weak_refs_with_cleanup_some, \
"harmony weak references with FinalizationRegistry.prototype.cleanupSome") \
V(harmony_import_assertions, "harmony import assertions")
V(harmony_import_assertions, "harmony import assertions") \
V(harmony_private_brand_checks, "harmony private brand checks")
#ifdef V8_INTL_SUPPORT
#define HARMONY_INPROGRESS(V) \

View File

@ -432,7 +432,7 @@ MaybeHandle<Object> LoadIC::Load(Handle<Object> object, Handle<Name> name,
LookupForRead(&it, IsAnyHas());
if (name->IsPrivate()) {
if (name->IsPrivateName() && !it.IsFound()) {
if (!IsAnyHas() && name->IsPrivateName() && !it.IsFound()) {
Handle<String> name_string(
String::cast(Symbol::cast(*name).description()), isolate());
if (name->IsPrivateBrand()) {

View File

@ -4320,6 +4320,7 @@ EMPTY_INITIALIZE_GLOBAL_FOR_FEATURE(harmony_regexp_sequence)
EMPTY_INITIALIZE_GLOBAL_FOR_FEATURE(harmony_top_level_await)
EMPTY_INITIALIZE_GLOBAL_FOR_FEATURE(harmony_logical_assignment)
EMPTY_INITIALIZE_GLOBAL_FOR_FEATURE(harmony_import_assertions)
EMPTY_INITIALIZE_GLOBAL_FOR_FEATURE(harmony_private_brand_checks)
#ifdef V8_INTL_SUPPORT
EMPTY_INITIALIZE_GLOBAL_FOR_FEATURE(harmony_intl_displaynames_date_types)

View File

@ -4792,6 +4792,65 @@ void BytecodeGenerator::BuildPrivateSetterAccess(Register object,
feedback_index(feedback_spec()->AddCallICSlot()));
}
void BytecodeGenerator::BuildPrivateMethodIn(Variable* private_name,
Expression* object_expression) {
DCHECK(IsPrivateMethodOrAccessorVariableMode(private_name->mode()));
ClassScope* scope = private_name->scope()->AsClassScope();
if (private_name->is_static()) {
// For static private methods, "#privatemethod in ..." only returns true for
// the class constructor.
if (scope->class_variable() == nullptr) {
// Can only happen via the debugger. See comment in
// BuildPrivateBrandCheck.
RegisterAllocationScope register_scope(this);
RegisterList args = register_allocator()->NewRegisterList(2);
builder()
->LoadLiteral(Smi::FromEnum(
MessageTemplate::
kInvalidUnusedPrivateStaticMethodAccessedByDebugger))
.StoreAccumulatorInRegister(args[0])
.LoadLiteral(private_name->raw_name())
.StoreAccumulatorInRegister(args[1])
.CallRuntime(Runtime::kNewError, args)
.Throw();
} else {
VisitForAccumulatorValue(object_expression);
Register object = register_allocator()->NewRegister();
builder()->StoreAccumulatorInRegister(object);
BytecodeLabel is_object;
builder()->JumpIfJSReceiver(&is_object);
RegisterList args = register_allocator()->NewRegisterList(3);
builder()
->StoreAccumulatorInRegister(args[2])
.LoadLiteral(Smi::FromEnum(MessageTemplate::kInvalidInOperatorUse))
.StoreAccumulatorInRegister(args[0])
.LoadLiteral(private_name->raw_name())
.StoreAccumulatorInRegister(args[1])
.CallRuntime(Runtime::kNewTypeError, args)
.Throw();
builder()->Bind(&is_object);
BuildVariableLoadForAccumulatorValue(scope->class_variable(),
HoleCheckMode::kElided);
builder()->CompareReference(object);
}
} else {
BuildVariableLoadForAccumulatorValue(scope->brand(),
HoleCheckMode::kElided);
Register brand = register_allocator()->NewRegister();
builder()->StoreAccumulatorInRegister(brand);
VisitForAccumulatorValue(object_expression);
builder()->SetExpressionPosition(object_expression);
FeedbackSlot slot = feedback_spec()->AddKeyedHasICSlot();
builder()->CompareOperation(Token::IN, brand, feedback_index(slot));
execution_result()->SetResultIsBoolean();
}
}
void BytecodeGenerator::BuildPrivateBrandCheck(Property* property,
Register object,
MessageTemplate tmpl) {
@ -5660,6 +5719,16 @@ void BytecodeGenerator::VisitCompareOperation(CompareOperation* expr) {
builder()->SetExpressionPosition(expr);
BuildLiteralCompareNil(expr->op(), BytecodeArrayBuilder::kNullValue);
} else {
if (expr->op() == Token::IN && expr->left()->IsPrivateName()) {
DCHECK(FLAG_harmony_private_brand_checks);
Variable* var = expr->left()->AsVariableProxy()->var();
if (IsPrivateMethodOrAccessorVariableMode(var->mode())) {
BuildPrivateMethodIn(var, expr->right());
return;
}
// For private fields, the code below does the right thing.
}
Register lhs = VisitForRegisterValue(expr->left());
VisitForAccumulatorValue(expr->right());
builder()->SetExpressionPosition(expr);

View File

@ -312,6 +312,8 @@ class BytecodeGenerator final : public AstVisitor<BytecodeGenerator> {
void BuildInvalidPropertyAccess(MessageTemplate tmpl, Property* property);
void BuildPrivateBrandCheck(Property* property, Register object,
MessageTemplate tmpl);
void BuildPrivateMethodIn(Variable* private_name,
Expression* object_expression);
void BuildPrivateGetterAccess(Register obj, Register access_pair);
void BuildPrivateSetterAccess(Register obj, Register access_pair,
Register value);

View File

@ -3144,7 +3144,20 @@ template <typename Impl>
typename ParserBase<Impl>::ExpressionT ParserBase<Impl>::ParseBinaryExpression(
int prec) {
DCHECK_GE(prec, 4);
ExpressionT x = ParseUnaryExpression();
ExpressionT x;
// "#foo in ShiftExpression" needs to be parsed separately, since private
// identifiers are not valid PrimaryExpressions.
if (V8_UNLIKELY(FLAG_harmony_private_brand_checks &&
peek() == Token::PRIVATE_NAME)) {
x = ParsePropertyOrPrivatePropertyName();
if (peek() != Token::IN) {
ReportUnexpectedToken(peek());
return impl()->FailureExpression();
}
} else {
x = ParseUnaryExpression();
}
int prec1 = Token::Precedence(peek(), accept_IN_);
if (prec1 >= prec) {
return ParseBinaryContinuation(x, prec, prec1);

View File

@ -0,0 +1,567 @@
// Copyright 2021 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --harmony-private-brand-checks --allow-natives-syntax
// Objects for which all our brand checks return false.
const commonFalseCases = [{}, function() {}, []];
// Values for which all our brand checks throw.
const commonThrowCases = [100, 'foo', undefined, null];
(function TestReturnValue() {
class A {
m() {
assertEquals(typeof (#x in this), 'boolean');
assertEquals(typeof (#x in {}), 'boolean');
}
#x = 1;
}
})();
(function TestPrivateField() {
class A {
m(other) {
return #x in other;
}
#x = 1;
}
let a = new A();
assertTrue(a.m(a));
assertTrue(a.m(new A()));
for (o of commonFalseCases) {
assertFalse(a.m(o));
}
for (o of commonThrowCases) {
assertThrows(() => { a.m(o) }, TypeError);
}
class B {
#x = 5;
}
assertFalse(a.m(new B()));
})();
(function TestPrivateFieldWithValueUndefined() {
class A {
m(other) {
return #x in other;
}
#x;
}
let a = new A();
assertTrue(a.m(a));
assertTrue(a.m(new A()));
for (o of commonFalseCases) {
assertFalse(a.m(o));
}
for (o of commonThrowCases) {
assertThrows(() => { a.m(o) }, TypeError);
}
class B {
#x;
}
assertFalse(a.m(new B()));
})();
(function TestPrivateMethod() {
class A {
#pm() {
}
m(other) {
return #pm in other;
}
}
let a = new A();
assertTrue(a.m(a));
assertTrue(a.m(new A()));
for (o of commonFalseCases) {
assertFalse(a.m(o));
}
for (o of commonThrowCases) {
assertThrows(() => { a.m(o) }, TypeError);
}
class B {
#pm() {}
}
assertFalse(a.m(new B()));
})();
(function TestPrivateGetter() {
class A {
get #foo() {
}
m(other) {
return #foo in other;
}
}
let a = new A();
assertTrue(a.m(a));
assertTrue(a.m(new A()));
for (o of commonFalseCases) {
assertFalse(a.m(o));
}
for (o of commonThrowCases) {
assertThrows(() => { a.m(o) }, TypeError);
}
class B {
get #foo() {}
}
assertFalse(a.m(new B()));
})();
(function TestPrivateSetter() {
class A {
set #foo(a) {
}
m(other) {
return #foo in other;
}
}
let a = new A();
assertTrue(a.m(a));
assertTrue(a.m(new A()));
for (o of commonFalseCases) {
assertFalse(a.m(o));
}
for (o of commonThrowCases) {
assertThrows(() => { a.m(o) }, TypeError);
}
class B {
set #foo(a) {}
}
assertFalse(a.m(new B()));
})();
(function TestPrivateGetterAndSetter() {
class A {
get #foo() {}
set #foo(a) {
}
m(other) {
return #foo in other;
}
}
let a = new A();
assertTrue(a.m(a));
assertTrue(a.m(new A()));
for (o of commonFalseCases) {
assertFalse(a.m(o));
}
for (o of commonThrowCases) {
assertThrows(() => { a.m(o) }, TypeError);
}
class B {
get #foo() {}
set #foo(a) {}
}
assertFalse(a.m(new B()));
})();
(function TestPrivateStaticField() {
class A {
static m(other) {
return #x in other;
}
static #x = 1;
}
assertTrue(A.m(A));
assertFalse(A.m(new A()));
for (o of commonFalseCases) {
assertFalse(A.m(o));
}
for (o of commonThrowCases) {
assertThrows(() => { A.m(o) }, TypeError);
}
class B {
static #x = 5;
}
assertFalse(A.m(B));
})();
(function TestPrivateStaticMethod() {
class A {
static m(other) {
return #pm in other;
}
static #pm() {}
}
assertTrue(A.m(A));
assertFalse(A.m(new A()));
for (o of commonFalseCases) {
assertFalse(A.m(o));
}
for (o of commonThrowCases) {
assertThrows(() => { A.m(o) }, TypeError);
}
class B {
static #pm() {};
}
assertFalse(A.m(B));
})();
(function TestPrivateStaticGetter() {
class A {
static m(other) {
return #x in other;
}
static get #x() {}
}
assertTrue(A.m(A));
assertFalse(A.m(new A()));
for (o of commonFalseCases) {
assertFalse(A.m(o));
}
for (o of commonThrowCases) {
assertThrows(() => { A.m(o) }, TypeError);
}
class B {
static get #x() {};
}
assertFalse(A.m(B));
})();
(function TestPrivateStaticSetter() {
class A {
static m(other) {
return #x in other;
}
static set #x(x) {}
}
assertTrue(A.m(A));
assertFalse(A.m(new A()));
for (o of commonFalseCases) {
assertFalse(A.m(o));
}
for (o of commonThrowCases) {
assertThrows(() => { A.m(o) }, TypeError);
}
class B {
static set #x(x) {};
}
assertFalse(A.m(B));
})();
(function TestPrivateStaticGetterAndSetter() {
class A {
static m(other) {
return #x in other;
}
static get #x() {}
static set #x(x) {}
}
assertTrue(A.m(A));
assertFalse(A.m(new A()));
for (o of commonFalseCases) {
assertFalse(A.m(o));
}
for (o of commonThrowCases) {
assertThrows(() => { A.m(o) }, TypeError);
}
class B {
static get #x() {}
static set #x(x) {};
}
assertFalse(A.m(B));
})();
(function TestPrivateIdentifiersAreDistinct() {
function GenerateClass() {
class A {
m(other) {
return #x in other;
}
#x = 0;
}
return new A();
}
let a1 = GenerateClass();
let a2 = GenerateClass();
assertTrue(a1.m(a1));
assertFalse(a1.m(a2));
assertFalse(a2.m(a1));
assertTrue(a2.m(a2));
})();
(function TestSubclasses() {
class A {
m(other) { return #foo in other; }
#foo;
}
class B extends A {}
assertTrue((new A()).m(new B()));
})();
(function TestFakeSubclassesWithPrivateField() {
class A {
#foo;
m() { return #foo in this; }
}
let a = new A();
assertTrue(a.m());
// Plug an object into the prototype chain; it's not a real instance of the
// class.
let fake = {__proto__: a};
assertFalse(fake.m());
})();
(function TestFakeSubclassesWithPrivateMethod() {
class A {
#pm() {}
m() { return #pm in this; }
}
let a = new A();
assertTrue(a.m());
// Plug an object into the prototype chain; it's not a real instance of the
// class.
let fake = {__proto__: a};
assertFalse(fake.m());
})();
(function TestPrivateNameUnknown() {
assertThrows(() => { eval(`
class A {
m(other) { return #lol in other; }
}
new A().m();
`)}, SyntaxError, /must be declared in an enclosing class/);
})();
(function TestEvalWithPrivateField() {
class A {
m(other) {
let result;
eval('result = #x in other;');
return result;
}
#x = 1;
}
let a = new A();
assertTrue(a.m(a));
assertTrue(a.m(new A()));
for (o of commonFalseCases) {
assertFalse(a.m(o));
}
for (o of commonThrowCases) {
assertThrows(() => { a.m(o) }, TypeError);
}
})();
(function TestEvalWithPrivateMethod() {
class A {
m(other) {
let result;
eval('result = #pm in other;');
return result;
}
#pm() {}
}
let a = new A();
assertTrue(a.m(a));
assertTrue(a.m(new A()));
for (o of commonFalseCases) {
assertFalse(a.m(o));
}
for (o of commonThrowCases) {
assertThrows(() => { a.m(o) }, TypeError);
}
})();
(function TestEvalWithStaticPrivateField() {
class A {
static m(other) {
let result;
eval('result = #x in other;');
return result;
}
static #x = 1;
}
assertTrue(A.m(A));
assertFalse(A.m(new A()));
for (o of commonFalseCases) {
assertFalse(A.m(o));
}
for (o of commonThrowCases) {
assertThrows(() => { A.m(o) }, TypeError);
}
})();
(function TestEvalWithStaticPrivateMethod() {
class A {
static m(other) {
let result;
eval('result = #pm in other;');
return result;
}
static #pm() {}
}
assertTrue(A.m(A));
assertFalse(A.m(new A()));
for (o of commonFalseCases) {
assertFalse(A.m(o));
}
for (o of commonThrowCases) {
assertThrows(() => { A.m(o) }, TypeError);
}
})();
(function TestCombiningWithOtherExpressions() {
class A {
m() {
assertFalse(#x in {} in {} in {});
assertTrue(#x in this in {true: 0});
assertTrue(#x in {} < 1 + 1);
assertFalse(#x in this < 1);
assertThrows(() => { eval('#x in {} = 4')});
assertThrows(() => { eval('(#x in {}) = 4')});
}
#x;
}
new A().m();
})();
(function TestHalfConstructedObjects() {
let half_constructed;
class A {
m() {
assertTrue(#x in this);
assertFalse(#y in this);
}
#x = 0;
#y = (() => { half_constructed = this; throw 'lol';})();
}
try {
new A();
} catch {
}
half_constructed.m();
})();
(function TestPrivateFieldOpt() {
class A {
m(other) {
return #x in other;
}
#x = 1;
}
let a = new A();
%PrepareFunctionForOptimization(A.prototype.m);
assertTrue(a.m(a));
assertTrue(a.m(new A()));
%OptimizeFunctionOnNextCall(A.prototype.m);
assertTrue(a.m(a));
assertTrue(a.m(new A()));
class B {
#x = 5;
}
assertFalse(a.m(new B()));
})();
(function TestPrivateMethodOpt() {
class A {
#pm() {
}
m(other) {
return #pm in other;
}
}
let a = new A();
%PrepareFunctionForOptimization(A.prototype.m);
assertTrue(a.m(a));
assertTrue(a.m(new A()));
%OptimizeFunctionOnNextCall(A.prototype.m);
assertTrue(a.m(a));
assertTrue(a.m(new A()));
class B {
#pm() {}
}
assertFalse(a.m(new B()));
})();
(function TestPrivateStaticFieldOpt() {
class A {
static m(other) {
return #x in other;
}
static #x = 1;
}
%PrepareFunctionForOptimization(A.m);
assertTrue(A.m(A));
%OptimizeFunctionOnNextCall(A.m);
assertTrue(A.m(A));
class B {
static #x = 5;
}
assertFalse(A.m(B));
})();
(function TestPrivateStaticMethodOpt() {
class A {
static m(other) {
return #pm in other;
}
static #pm() {}
}
%PrepareFunctionForOptimization(A.m);
assertTrue(A.m(A));
%OptimizeFunctionOnNextCall(A.m);
assertTrue(A.m(A));
class B {
static #pm() {};
}
assertFalse(A.m(B));
})();
(function TestPrivateFieldWithProxy() {
class A {
m(other) {
return #x in other;
}
#x = 1;
}
let a = new A();
const p = new Proxy(a, {get: function() { assertUnreachable(); } });
assertFalse(a.m(p));
})();
(function TestHeritagePosition() {
class A {
#x; // A.#x
static C = class C extends (function () {
return class D {
exfil(obj) { return #x in obj; }
exfilEval(obj) { return eval("#x in obj"); }
};
}) { // C body starts
#x; // C.#x
} // C body ends
} // A ends
let c = new A.C();
let d = new c();
// #x inside D binds to A.#x, so only objects of A pass the check.
assertTrue(d.exfil(new A()));
assertFalse(d.exfil(c));
assertFalse(d.exfil(d));
assertTrue(d.exfilEval(new A()));
assertFalse(d.exfilEval(c));
assertFalse(d.exfilEval(d));
})();