Add Context::DeepFreeze

Change-Id: I1002944931fa7705048457e2cd2c39494923c750
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3691125
Reviewed-by: Camillo Bruni <cbruni@chromium.org>
Commit-Queue: Russ Hamilton <behamilton@google.com>
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
Cr-Commit-Position: refs/heads/main@{#85710}
This commit is contained in:
Russ Hamilton 2023-02-07 15:10:28 +00:00 committed by V8 LUCI CQ
parent d367ee2ac6
commit 2833957c77
5 changed files with 489 additions and 1 deletions

View File

@ -9,6 +9,7 @@
#include "v8-data.h" // NOLINT(build/include_directory) #include "v8-data.h" // NOLINT(build/include_directory)
#include "v8-local-handle.h" // NOLINT(build/include_directory) #include "v8-local-handle.h" // NOLINT(build/include_directory)
#include "v8-maybe.h" // NOLINT(build/include_directory)
#include "v8-snapshot.h" // NOLINT(build/include_directory) #include "v8-snapshot.h" // NOLINT(build/include_directory)
#include "v8config.h" // NOLINT(build/include_directory) #include "v8config.h" // NOLINT(build/include_directory)
@ -163,6 +164,13 @@ class V8_EXPORT Context : public Data {
*/ */
void Exit(); void Exit();
/**
* Attempts to recursively freeze all objects reachable from this context.
* Some objects (generators, iterators, non-const closures) can not be frozen
* and will cause this method to throw an error.
*/
Maybe<void> DeepFreeze();
/** Returns the isolate associated with a current context. */ /** Returns the isolate associated with a current context. */
Isolate* GetIsolate(); Isolate* GetIsolate();

View File

@ -47,6 +47,7 @@
#include "src/compiler-dispatcher/lazy-compile-dispatcher.h" #include "src/compiler-dispatcher/lazy-compile-dispatcher.h"
#include "src/date/date.h" #include "src/date/date.h"
#include "src/objects/primitive-heap-object.h" #include "src/objects/primitive-heap-object.h"
#include "src/utils/identity-map.h"
#if V8_ENABLE_WEBASSEMBLY #if V8_ENABLE_WEBASSEMBLY
#include "src/debug/debug-wasm-objects.h" #include "src/debug/debug-wasm-objects.h"
#endif // V8_ENABLE_WEBASSEMBLY #endif // V8_ENABLE_WEBASSEMBLY
@ -85,6 +86,7 @@
#include "src/objects/embedder-data-slot-inl.h" #include "src/objects/embedder-data-slot-inl.h"
#include "src/objects/hash-table-inl.h" #include "src/objects/hash-table-inl.h"
#include "src/objects/heap-object.h" #include "src/objects/heap-object.h"
#include "src/objects/instance-type.h"
#include "src/objects/js-array-buffer-inl.h" #include "src/objects/js-array-buffer-inl.h"
#include "src/objects/js-array-inl.h" #include "src/objects/js-array-inl.h"
#include "src/objects/js-collection-inl.h" #include "src/objects/js-collection-inl.h"
@ -6717,6 +6719,218 @@ Local<Value> v8::Context::GetSecurityToken() {
return Utils::ToLocal(token_handle); return Utils::ToLocal(token_handle);
} }
namespace {
bool MayContainObjectsToFreeze(i::InstanceType obj_type) {
if (i::InstanceTypeChecker::IsString(obj_type)) return false;
if (i::InstanceTypeChecker::IsSharedFunctionInfo(obj_type)) return false;
return true;
}
bool IsJSReceiverSafeToFreeze(i::InstanceType obj_type) {
DCHECK(i::InstanceTypeChecker::IsJSReceiver(obj_type));
switch (obj_type) {
case i::JS_OBJECT_TYPE:
case i::JS_GLOBAL_OBJECT_TYPE:
case i::JS_GLOBAL_PROXY_TYPE:
case i::JS_PRIMITIVE_WRAPPER_TYPE:
case i::JS_FUNCTION_TYPE:
/* Function types */
case i::BIGINT64_TYPED_ARRAY_CONSTRUCTOR_TYPE:
case i::BIGUINT64_TYPED_ARRAY_CONSTRUCTOR_TYPE:
case i::FLOAT32_TYPED_ARRAY_CONSTRUCTOR_TYPE:
case i::FLOAT64_TYPED_ARRAY_CONSTRUCTOR_TYPE:
case i::INT16_TYPED_ARRAY_CONSTRUCTOR_TYPE:
case i::INT32_TYPED_ARRAY_CONSTRUCTOR_TYPE:
case i::INT8_TYPED_ARRAY_CONSTRUCTOR_TYPE:
case i::UINT16_TYPED_ARRAY_CONSTRUCTOR_TYPE:
case i::UINT32_TYPED_ARRAY_CONSTRUCTOR_TYPE:
case i::UINT8_CLAMPED_TYPED_ARRAY_CONSTRUCTOR_TYPE:
case i::UINT8_TYPED_ARRAY_CONSTRUCTOR_TYPE:
case i::JS_ARRAY_CONSTRUCTOR_TYPE:
case i::JS_PROMISE_CONSTRUCTOR_TYPE:
case i::JS_REG_EXP_CONSTRUCTOR_TYPE:
case i::JS_CLASS_CONSTRUCTOR_TYPE:
/* Prototype Types */
case i::JS_ARRAY_ITERATOR_PROTOTYPE_TYPE:
case i::JS_ITERATOR_PROTOTYPE_TYPE:
case i::JS_MAP_ITERATOR_PROTOTYPE_TYPE:
case i::JS_OBJECT_PROTOTYPE_TYPE:
case i::JS_PROMISE_PROTOTYPE_TYPE:
case i::JS_REG_EXP_PROTOTYPE_TYPE:
case i::JS_SET_ITERATOR_PROTOTYPE_TYPE:
case i::JS_SET_PROTOTYPE_TYPE:
case i::JS_STRING_ITERATOR_PROTOTYPE_TYPE:
case i::JS_TYPED_ARRAY_PROTOTYPE_TYPE:
/* */
case i::JS_ARRAY_TYPE:
return true;
#if V8_ENABLE_WEBASSEMBLY
case i::WASM_ARRAY_TYPE:
case i::WASM_STRUCT_TYPE:
#endif // V8_ENABLE_WEBASSEMBLY
case i::JS_PROXY_TYPE:
return true;
// These types are known not to freeze.
case i::JS_MAP_KEY_ITERATOR_TYPE:
case i::JS_MAP_KEY_VALUE_ITERATOR_TYPE:
case i::JS_MAP_VALUE_ITERATOR_TYPE:
case i::JS_SET_KEY_VALUE_ITERATOR_TYPE:
case i::JS_SET_VALUE_ITERATOR_TYPE:
case i::JS_GENERATOR_OBJECT_TYPE:
case i::JS_ASYNC_FUNCTION_OBJECT_TYPE:
case i::JS_ASYNC_GENERATOR_OBJECT_TYPE:
case i::JS_ARRAY_ITERATOR_TYPE: {
return false;
}
default:
// TODO(behamilton): Handle any types that fall through here.
return false;
}
}
class ObjectVisitorDeepFreezer : i::ObjectVisitor {
public:
explicit ObjectVisitorDeepFreezer(i::Isolate* isolate) : isolate_(isolate) {}
bool DeepFreeze(i::Handle<i::Context> context) {
bool success = VisitObject(*i::Handle<i::HeapObject>::cast(context));
DCHECK_EQ(success, !error_.has_value());
if (!success) {
THROW_NEW_ERROR_RETURN_VALUE(
isolate_, NewTypeError(error_->msg_id, error_->name), false);
}
for (const auto& obj : objects_to_freeze_) {
MAYBE_RETURN_ON_EXCEPTION_VALUE(
isolate_,
i::JSReceiver::SetIntegrityLevel(isolate_, obj, i::FROZEN,
i::kThrowOnError),
false);
}
return true;
}
void VisitPointers(i::HeapObject host, i::ObjectSlot start,
i::ObjectSlot end) final {
VisitPointersImpl(start, end);
}
void VisitPointers(i::HeapObject host, i::MaybeObjectSlot start,
i::MaybeObjectSlot end) final {
VisitPointersImpl(start, end);
}
void VisitMapPointer(i::HeapObject host) final {
VisitPointer(host, host.map_slot());
}
void VisitCodePointer(i::HeapObject host, i::CodeObjectSlot slot) final {}
void VisitCodeTarget(i::InstructionStream host, i::RelocInfo* rinfo) final {}
void VisitEmbeddedPointer(i::InstructionStream host,
i::RelocInfo* rinfo) final {}
void VisitCustomWeakPointers(i::HeapObject host, i::ObjectSlot start,
i::ObjectSlot end) final {}
private:
struct ErrorInfo {
i::MessageTemplate msg_id;
i::Handle<i::String> name;
};
template <typename TSlot>
void VisitPointersImpl(TSlot start, TSlot end) {
for (TSlot current = start; current < end; ++current) {
typename TSlot::TObject object = current.load(isolate_);
i::HeapObject heap_object;
if (object.GetHeapObjectIfStrong(&heap_object)) {
if (!VisitObject(heap_object)) {
return;
}
}
}
}
bool VisitObject(i::HeapObject obj) {
DCHECK(!error_.has_value());
DCHECK(!obj.is_null());
i::DisallowGarbageCollection no_gc;
i::InstanceType obj_type = obj.map().instance_type();
// Skip common types that can't contain items to freeze.
if (!MayContainObjectsToFreeze(obj_type)) {
return true;
}
if (!done_list_.insert(obj).second) {
// If we couldn't insert (because it is already in the set) then we're
// done.
return true;
}
// For contexts we need to ensure that all accessible locals are const.
// If not they could be replaced to bypass freezing.
if (i::InstanceTypeChecker::IsContext(obj_type)) {
i::ScopeInfo scope_info = i::Context::cast(obj).scope_info();
for (auto it : i::ScopeInfo::IterateLocalNames(&scope_info, no_gc)) {
if (scope_info.ContextLocalMode(it->index()) !=
i::VariableMode::kConst) {
DCHECK(!error_.has_value());
error_ = ErrorInfo{i::MessageTemplate::kCannotDeepFreezeValue,
i::handle(it->name(), isolate_)};
return false;
}
}
} else if (i::InstanceTypeChecker::IsJSReceiver(obj_type)) {
i::Handle<i::JSReceiver> receiver =
i::handle(i::JSReceiver::cast(obj), isolate_);
if (!IsJSReceiverSafeToFreeze(obj_type)) {
DCHECK(!error_.has_value());
error_ = ErrorInfo{i::MessageTemplate::kCannotDeepFreezeObject,
i::handle(receiver->class_name(), isolate_)};
return false;
}
// Save this to freeze after we are done. Freezing triggers garbage
// collection which doesn't work well with this visitor pattern, so we
// delay it until after.
objects_to_freeze_.push_back(receiver);
} else {
DCHECK(!i::InstanceTypeChecker::IsContext(obj_type) &&
!i::InstanceTypeChecker::IsJSReceiver(obj_type));
}
DCHECK(!error_.has_value());
obj.Iterate(isolate_, this);
// Iterate sets error_ on failure. We should propagate errors.
return !error_.has_value();
}
i::Isolate* isolate_;
std::unordered_set<i::Object, i::Object::Hasher> done_list_;
std::vector<i::Handle<i::JSReceiver>> objects_to_freeze_;
base::Optional<ErrorInfo> error_;
};
} // namespace
Maybe<void> Context::DeepFreeze() {
i::Handle<i::Context> env = Utils::OpenHandle(this);
i::Isolate* i_isolate = env->GetIsolate();
// TODO(behamilton): Incorporate compatibility improvements similar to NodeJS:
// https://github.com/nodejs/node/blob/main/lib/internal/freeze_intrinsics.js
// These need to be done before freezing.
Local<Context> context = Utils::ToLocal(env);
ENTER_V8_NO_SCRIPT(i_isolate, context, Context, DeepFreeze, Nothing<void>(),
i::HandleScope);
ObjectVisitorDeepFreezer vfreezer(i_isolate);
has_pending_exception = !vfreezer.DeepFreeze(env);
RETURN_ON_FAILED_EXECUTION_PRIMITIVE(void);
return JustVoid();
}
v8::Isolate* Context::GetIsolate() { v8::Isolate* Context::GetIsolate() {
i::Handle<i::Context> env = Utils::OpenHandle(this); i::Handle<i::Context> env = Utils::OpenHandle(this);
return reinterpret_cast<Isolate*>(env->GetIsolate()); return reinterpret_cast<Isolate*>(env->GetIsolate());

View File

@ -711,7 +711,10 @@ namespace internal {
T(OptionalChainingNoSuper, "Invalid optional chain from super property") \ T(OptionalChainingNoSuper, "Invalid optional chain from super property") \
T(OptionalChainingNoTemplate, "Invalid tagged template on optional chain") \ T(OptionalChainingNoTemplate, "Invalid tagged template on optional chain") \
/* AggregateError */ \ /* AggregateError */ \
T(AllPromisesRejected, "All promises were rejected") T(AllPromisesRejected, "All promises were rejected") \
T(CannotDeepFreezeObject, "Cannot DeepFreeze object of type %") \
T(CannotDeepFreezeValue, "Cannot DeepFreeze non-const value %")
enum class MessageTemplate { enum class MessageTemplate {
#define TEMPLATE(NAME, STRING) k##NAME, #define TEMPLATE(NAME, STRING) k##NAME,
MESSAGE_TEMPLATES(TEMPLATE) MESSAGE_TEMPLATES(TEMPLATE)

View File

@ -144,6 +144,7 @@ class RuntimeCallTimer final {
V(BigUint64Array_New) \ V(BigUint64Array_New) \
V(BooleanObject_BooleanValue) \ V(BooleanObject_BooleanValue) \
V(BooleanObject_New) \ V(BooleanObject_New) \
V(Context_DeepFreeze) \
V(Context_New) \ V(Context_New) \
V(Context_NewRemoteContext) \ V(Context_NewRemoteContext) \
V(DataView_New) \ V(DataView_New) \

View File

@ -29553,3 +29553,265 @@ TEST(WasmAbortStreamingAfterContextDisposal) {
wasm_streaming.reset(); wasm_streaming.reset();
} }
#endif // V8_ENABLE_WEBASSEMBLY #endif // V8_ENABLE_WEBASSEMBLY
TEST(DeepFreezeIncompatibleTypes) {
const int numCases = 7;
struct {
const char* script;
const char* exception;
} test_cases[numCases] = {
{
R"(
"use strict"
let foo = 1;
)",
"TypeError: Cannot DeepFreeze non-const value foo"},
{
R"(
"use strict"
const foo = 1;
const generator = function*() {
yield 1;
yield 2;
}
const gen = generator();
)",
"TypeError: Cannot DeepFreeze object of type Generator"},
{
R"(
"use strict"
const incrementer = (function() {
let a = 1;
return function() { a += 1; return a; };
})();
)",
"TypeError: Cannot DeepFreeze non-const value a"},
{
R"(
let a = new Number();
)",
"TypeError: Cannot DeepFreeze non-const value a"},
{
R"(
const a = [0, 1, 2, 3, 4, 5];
var it = a[Symbol.iterator]();
function foo() {
return it.next().value;
}
foo();
)",
"TypeError: Cannot DeepFreeze object of type Array Iterator"},
{
R"(
const a = "0123456789";
var it = a[Symbol.iterator]();
function foo() {
return it.next().value;
}
foo();
)",
"TypeError: Cannot DeepFreeze object of type Object"},
{R"(
const a = "0123456789";
var it = a.matchAll(/\d/g);
function foo() {
return it.next().value;
}
foo();
)",
"TypeError: Cannot DeepFreeze object of type Object"},
};
for (int idx = 0; idx < numCases; idx++) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope scope(isolate);
v8::Local<v8::Context> context = env.local();
v8::Maybe<void> maybe_success = v8::Nothing<void>();
CompileRun(context, test_cases[idx].script);
v8::TryCatch tc(isolate);
maybe_success = context->DeepFreeze();
CHECK(maybe_success.IsNothing());
CHECK(tc.HasCaught());
v8::String::Utf8Value uS(isolate, tc.Exception());
std::string exception(*uS, uS.length());
CHECK_EQ(std::string(test_cases[idx].exception), exception);
}
}
TEST(DeepFreezeIsFrozen) {
const int numCases = 10;
struct {
const char* script;
const char* exception;
int32_t expected;
} test_cases[numCases] = {
{// Closure
R"(
const incrementer = (function() {
const a = {b: 1};
return function() { a.b += 1; return a.b; };
})();
const foo = function() { return incrementer(); }
foo();
)",
nullptr, 2},
{
R"(
const incrementer = (function() {
const a = {b: 1};
return function() { a.b += 1; return a.b; };
})();
const foo = function() { return incrementer(); }
foo();
)",
nullptr, 2},
{// Array
R"(
const a = [0, -1, -2];
const foo = function() { a[0] += 1; return a[0]; }
)",
nullptr, 0},
{
R"(
const a = [0, -1, -2];
const foo = function() { a[0] += 1; return a[0]; }
)",
nullptr, 0},
{// Wrapper Objects
R"(
const a = {b: new Number()};
const foo = function() {
a.b = new Number(a.b + 1);
return a.b.valueOf();
}
)",
nullptr, 0},
{// Functions
// Assignment to constant doesn't work.
R"(
const foo = function() {
foo = function() { return 2;}
return 1;
}
)",
"TypeError: Assignment to constant variable.", 0},
{
R"(
const a = {b: {c: {d: {e: {f: 1}}}}};
const foo = function() {
a.b.c.d.e.f += 1;
return a.b.c.d.e.f;
}
)",
nullptr, 1},
{
R"(
const foo = function() {
if (!('count' in globalThis))
globalThis.count = 1;
++count;
return count;
}
)",
"ReferenceError: count is not defined", 0},
{
R"(
const countPrototype = {
get() {
return 1;
},
};
const count = Object.create(countPrototype);
function foo() {
const curr_count = count.get();
count.prototype = { get() { return curr_count + 1; }};
return count.get();
}
)",
nullptr, 1},
{
R"(
const a = (function(){
function A(){};
A.o = 1;
return new A();
})();
function foo() {
a.constructor.o++;
return a.constructor.o;
}
)",
nullptr, 1},
};
for (int idx = 0; idx < numCases; idx++) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope scope(isolate);
v8::Local<v8::Context> context = env.local();
v8::Maybe<void> maybe_success = v8::Nothing<void>();
v8::TryCatch tc(isolate);
v8::MaybeLocal<v8::Value> status =
CompileRun(context, test_cases[idx].script);
CHECK(!status.IsEmpty());
CHECK(!tc.HasCaught());
maybe_success = context->DeepFreeze();
CHECK(!tc.HasCaught());
status = CompileRun(context, "foo()");
if (test_cases[idx].exception) {
CHECK(tc.HasCaught());
v8::String::Utf8Value uS(isolate, tc.Exception());
std::string exception(*uS, uS.length());
CHECK_EQ(std::string(test_cases[idx].exception), exception);
} else {
CHECK(!tc.HasCaught());
CHECK(!status.IsEmpty());
ExpectInt32("foo()", test_cases[idx].expected);
}
}
}
TEST(DeepFreezeAllowsSyntax) {
const int numCases = 2;
struct {
const char* script;
int32_t expected;
} test_cases[numCases] = {
{
R"(
const a = 1;
function foo() {
let b = 4;
b += 1;
return a + b;
}
)",
6,
},
{
R"(
var a = 1;
function foo() {
let b = 4;
b += 1;
return a + b;
}
)",
6,
}}; // TODO(behamilton): Add more cases that should be supported.
for (int idx = 0; idx < numCases; idx++) {
LocalContext env;
v8::Isolate* isolate = env->GetIsolate();
v8::HandleScope scope(isolate);
v8::Local<v8::Context> context = env.local();
v8::Maybe<void> maybe_success = v8::Nothing<void>();
v8::MaybeLocal<v8::Value> status =
CompileRun(context, test_cases[idx].script);
CHECK(!status.IsEmpty());
maybe_success = context->DeepFreeze();
CHECK(!maybe_success.IsNothing());
ExpectInt32("foo()", test_cases[idx].expected);
}
}