diff --git a/include/v8-context.h b/include/v8-context.h index 0e6dc9a59b..78015a72cc 100644 --- a/include/v8-context.h +++ b/include/v8-context.h @@ -9,6 +9,7 @@ #include "v8-data.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 "v8config.h" // NOLINT(build/include_directory) @@ -163,6 +164,13 @@ class V8_EXPORT Context : public Data { */ 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 DeepFreeze(); + /** Returns the isolate associated with a current context. */ Isolate* GetIsolate(); diff --git a/src/api/api.cc b/src/api/api.cc index 3f1fcc65c5..f446c875fa 100644 --- a/src/api/api.cc +++ b/src/api/api.cc @@ -47,6 +47,7 @@ #include "src/compiler-dispatcher/lazy-compile-dispatcher.h" #include "src/date/date.h" #include "src/objects/primitive-heap-object.h" +#include "src/utils/identity-map.h" #if V8_ENABLE_WEBASSEMBLY #include "src/debug/debug-wasm-objects.h" #endif // V8_ENABLE_WEBASSEMBLY @@ -85,6 +86,7 @@ #include "src/objects/embedder-data-slot-inl.h" #include "src/objects/hash-table-inl.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-inl.h" #include "src/objects/js-collection-inl.h" @@ -6717,6 +6719,218 @@ Local v8::Context::GetSecurityToken() { 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 context) { + bool success = VisitObject(*i::Handle::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 name; + }; + + template + 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 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 done_list_; + std::vector> objects_to_freeze_; + base::Optional error_; +}; + +} // namespace + +Maybe Context::DeepFreeze() { + i::Handle 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 = Utils::ToLocal(env); + ENTER_V8_NO_SCRIPT(i_isolate, context, Context, DeepFreeze, Nothing(), + i::HandleScope); + ObjectVisitorDeepFreezer vfreezer(i_isolate); + has_pending_exception = !vfreezer.DeepFreeze(env); + + RETURN_ON_FAILED_EXECUTION_PRIMITIVE(void); + return JustVoid(); +} + v8::Isolate* Context::GetIsolate() { i::Handle env = Utils::OpenHandle(this); return reinterpret_cast(env->GetIsolate()); diff --git a/src/common/message-template.h b/src/common/message-template.h index 87de3c05a2..16d28beb5d 100644 --- a/src/common/message-template.h +++ b/src/common/message-template.h @@ -711,7 +711,10 @@ namespace internal { T(OptionalChainingNoSuper, "Invalid optional chain from super property") \ T(OptionalChainingNoTemplate, "Invalid tagged template on optional chain") \ /* 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 { #define TEMPLATE(NAME, STRING) k##NAME, MESSAGE_TEMPLATES(TEMPLATE) diff --git a/src/logging/runtime-call-stats.h b/src/logging/runtime-call-stats.h index fca5d54c31..2c2eb2b303 100644 --- a/src/logging/runtime-call-stats.h +++ b/src/logging/runtime-call-stats.h @@ -144,6 +144,7 @@ class RuntimeCallTimer final { V(BigUint64Array_New) \ V(BooleanObject_BooleanValue) \ V(BooleanObject_New) \ + V(Context_DeepFreeze) \ V(Context_New) \ V(Context_NewRemoteContext) \ V(DataView_New) \ diff --git a/test/cctest/test-api.cc b/test/cctest/test-api.cc index 2ade47697a..7621f3d754 100644 --- a/test/cctest/test-api.cc +++ b/test/cctest/test-api.cc @@ -29553,3 +29553,265 @@ TEST(WasmAbortStreamingAfterContextDisposal) { wasm_streaming.reset(); } #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 context = env.local(); + v8::Maybe maybe_success = v8::Nothing(); + 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 context = env.local(); + v8::Maybe maybe_success = v8::Nothing(); + v8::TryCatch tc(isolate); + v8::MaybeLocal 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 context = env.local(); + v8::Maybe maybe_success = v8::Nothing(); + v8::MaybeLocal status = + CompileRun(context, test_cases[idx].script); + CHECK(!status.IsEmpty()); + maybe_success = context->DeepFreeze(); + CHECK(!maybe_success.IsNothing()); + ExpectInt32("foo()", test_cases[idx].expected); + } +}