Upload Kmake

This commit is contained in:
Gorochu
2026-05-26 23:36:42 -07:00
parent ba051b2f74
commit 555ec72358
41615 changed files with 13344630 additions and 1 deletions

View File

@ -0,0 +1,659 @@
// Copyright 2018 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.
#include "src/execution/microtask-queue.h"
#include <algorithm>
#include <functional>
#include <memory>
#include <vector>
#include "include/v8-function.h"
#include "src/heap/factory.h"
#include "src/objects/foreign.h"
#include "src/objects/js-array-inl.h"
#include "src/objects/js-objects-inl.h"
#include "src/objects/objects-inl.h"
#include "src/objects/promise-inl.h"
#include "src/objects/visitors.h"
#include "test/unittests/test-utils.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace v8 {
namespace internal {
using Closure = std::function<void()>;
void RunStdFunction(void* data) {
std::unique_ptr<Closure> f(static_cast<Closure*>(data));
(*f)();
}
template <typename TMixin>
class WithFinalizationRegistryMixin : public TMixin {
public:
WithFinalizationRegistryMixin() = default;
~WithFinalizationRegistryMixin() override = default;
WithFinalizationRegistryMixin(const WithFinalizationRegistryMixin&) = delete;
WithFinalizationRegistryMixin& operator=(
const WithFinalizationRegistryMixin&) = delete;
static void SetUpTestSuite() {
CHECK_NULL(save_flags_);
save_flags_ = new SaveFlags();
v8_flags.expose_gc = true;
v8_flags.allow_natives_syntax = true;
TMixin::SetUpTestSuite();
}
static void TearDownTestSuite() {
TMixin::TearDownTestSuite();
CHECK_NOT_NULL(save_flags_);
delete save_flags_;
save_flags_ = nullptr;
}
private:
static SaveFlags* save_flags_;
};
template <typename TMixin>
SaveFlags* WithFinalizationRegistryMixin<TMixin>::save_flags_ = nullptr;
using TestWithNativeContextAndFinalizationRegistry = //
WithInternalIsolateMixin< //
WithContextMixin< //
WithFinalizationRegistryMixin< //
WithIsolateScopeMixin< //
WithIsolateMixin< //
WithDefaultPlatformMixin< //
::testing::Test>>>>>>;
namespace {
void DummyPromiseHook(PromiseHookType type, Local<Promise> promise,
Local<Value> parent) {}
} // namespace
class MicrotaskQueueTest : public TestWithNativeContextAndFinalizationRegistry,
public ::testing::WithParamInterface<bool> {
public:
template <typename F>
DirectHandle<Microtask> NewMicrotask(F&& f) {
DirectHandle<Foreign> runner = factory()->NewForeign<kMicrotaskCallbackTag>(
reinterpret_cast<Address>(&RunStdFunction));
DirectHandle<Foreign> data =
factory()->NewForeign<kMicrotaskCallbackDataTag>(
reinterpret_cast<Address>(new Closure(std::forward<F>(f))));
return factory()->NewCallbackTask(runner, data);
}
void SetUp() override {
microtask_queue_ = MicrotaskQueue::New(isolate());
native_context()->set_microtask_queue(isolate(), microtask_queue());
if (GetParam()) {
// Use a PromiseHook to switch the implementation to ResolvePromise
// runtime, instead of ResolvePromise builtin.
v8_isolate()->SetPromiseHook(&DummyPromiseHook);
}
}
void TearDown() override {
if (microtask_queue()) {
microtask_queue()->RunMicrotasks(isolate());
context()->DetachGlobal();
}
}
MicrotaskQueue* microtask_queue() const { return microtask_queue_.get(); }
void ClearTestMicrotaskQueue() {
context()->DetachGlobal();
microtask_queue_ = nullptr;
}
template <size_t N>
DirectHandle<Name> NameFromChars(const char (&chars)[N]) {
return isolate()->factory()->NewStringFromStaticChars(chars);
}
private:
std::unique_ptr<MicrotaskQueue> microtask_queue_;
};
class RecordingVisitor : public RootVisitor {
public:
RecordingVisitor() = default;
~RecordingVisitor() override = default;
void VisitRootPointers(Root root, const char* description,
FullObjectSlot start, FullObjectSlot end) override {
for (FullObjectSlot current = start; current != end; ++current) {
visited_.push_back(*current);
}
}
const std::vector<Tagged<Object>>& visited() const { return visited_; }
private:
std::vector<Tagged<Object>> visited_;
};
// Sanity check. Ensure a microtask is stored in a queue and run.
TEST_P(MicrotaskQueueTest, EnqueueAndRun) {
bool ran = false;
EXPECT_EQ(0, microtask_queue()->capacity());
EXPECT_EQ(0, microtask_queue()->size());
microtask_queue()->EnqueueMicrotask(*NewMicrotask([this, &ran] {
EXPECT_FALSE(ran);
ran = true;
EXPECT_TRUE(microtask_queue()->HasMicrotasksSuppressions());
}));
EXPECT_EQ(MicrotaskQueue::kMinimumCapacity, microtask_queue()->capacity());
EXPECT_EQ(1, microtask_queue()->size());
EXPECT_EQ(1, microtask_queue()->RunMicrotasks(isolate()));
EXPECT_TRUE(ran);
EXPECT_EQ(0, microtask_queue()->size());
}
// Check for a buffer growth.
TEST_P(MicrotaskQueueTest, BufferGrowth) {
int count = 0;
// Enqueue and flush the queue first to have non-zero |start_|.
microtask_queue()->EnqueueMicrotask(
*NewMicrotask([&count] { EXPECT_EQ(0, count++); }));
EXPECT_EQ(1, microtask_queue()->RunMicrotasks(isolate()));
EXPECT_LT(0, microtask_queue()->capacity());
EXPECT_EQ(0, microtask_queue()->size());
EXPECT_EQ(1, microtask_queue()->start());
// Fill the queue with Microtasks.
for (int i = 1; i <= MicrotaskQueue::kMinimumCapacity; ++i) {
microtask_queue()->EnqueueMicrotask(
*NewMicrotask([&count, i] { EXPECT_EQ(i, count++); }));
}
EXPECT_EQ(MicrotaskQueue::kMinimumCapacity, microtask_queue()->capacity());
EXPECT_EQ(MicrotaskQueue::kMinimumCapacity, microtask_queue()->size());
// Add another to grow the ring buffer.
microtask_queue()->EnqueueMicrotask(*NewMicrotask(
[&] { EXPECT_EQ(MicrotaskQueue::kMinimumCapacity + 1, count++); }));
EXPECT_LT(MicrotaskQueue::kMinimumCapacity, microtask_queue()->capacity());
EXPECT_EQ(MicrotaskQueue::kMinimumCapacity + 1, microtask_queue()->size());
// Run all pending Microtasks to ensure they run in the proper order.
EXPECT_EQ(MicrotaskQueue::kMinimumCapacity + 1,
microtask_queue()->RunMicrotasks(isolate()));
EXPECT_EQ(MicrotaskQueue::kMinimumCapacity + 2, count);
}
// MicrotaskQueue instances form a doubly linked list.
TEST_P(MicrotaskQueueTest, InstanceChain) {
ClearTestMicrotaskQueue();
MicrotaskQueue* default_mtq = isolate()->default_microtask_queue();
ASSERT_TRUE(default_mtq);
EXPECT_EQ(default_mtq, default_mtq->next());
EXPECT_EQ(default_mtq, default_mtq->prev());
// Create two instances, and check their connection.
// The list contains all instances in the creation order, and the next of the
// last instance is the first instance:
// default_mtq -> mtq1 -> mtq2 -> default_mtq.
std::unique_ptr<MicrotaskQueue> mtq1 = MicrotaskQueue::New(isolate());
std::unique_ptr<MicrotaskQueue> mtq2 = MicrotaskQueue::New(isolate());
EXPECT_EQ(default_mtq->next(), mtq1.get());
EXPECT_EQ(mtq1->next(), mtq2.get());
EXPECT_EQ(mtq2->next(), default_mtq);
EXPECT_EQ(default_mtq, mtq1->prev());
EXPECT_EQ(mtq1.get(), mtq2->prev());
EXPECT_EQ(mtq2.get(), default_mtq->prev());
// Deleted item should be also removed from the list.
mtq1 = nullptr;
EXPECT_EQ(default_mtq->next(), mtq2.get());
EXPECT_EQ(mtq2->next(), default_mtq);
EXPECT_EQ(default_mtq, mtq2->prev());
EXPECT_EQ(mtq2.get(), default_mtq->prev());
}
// Pending Microtasks in MicrotaskQueues are strong roots. Ensure they are
// visited exactly once.
TEST_P(MicrotaskQueueTest, VisitRoot) {
// Ensure that the ring buffer has separate in-use region.
for (int i = 0; i < MicrotaskQueue::kMinimumCapacity / 2 + 1; ++i) {
microtask_queue()->EnqueueMicrotask(*NewMicrotask([] {}));
}
EXPECT_EQ(MicrotaskQueue::kMinimumCapacity / 2 + 1,
microtask_queue()->RunMicrotasks(isolate()));
std::vector<Tagged<Object>> expected;
for (int i = 0; i < MicrotaskQueue::kMinimumCapacity / 2 + 1; ++i) {
DirectHandle<Microtask> microtask = NewMicrotask([] {});
expected.push_back(*microtask);
microtask_queue()->EnqueueMicrotask(*microtask);
}
EXPECT_GT(microtask_queue()->start() + microtask_queue()->size(),
microtask_queue()->capacity());
RecordingVisitor visitor;
microtask_queue()->IterateMicrotasks(&visitor);
std::vector<Tagged<Object>> actual = visitor.visited();
std::sort(expected.begin(), expected.end());
std::sort(actual.begin(), actual.end());
EXPECT_EQ(expected, actual);
}
TEST_P(MicrotaskQueueTest, PromiseHandlerContext) {
microtask_queue()->set_microtasks_policy(MicrotasksPolicy::kExplicit);
Local<v8::Context> v8_context2 = v8::Context::New(v8_isolate());
Local<v8::Context> v8_context3 = v8::Context::New(v8_isolate());
Local<v8::Context> v8_context4 = v8::Context::New(v8_isolate());
DirectHandle<Context> context2 =
Utils::OpenDirectHandle(*v8_context2, isolate());
DirectHandle<Context> context3 =
Utils::OpenDirectHandle(*v8_context3, isolate());
DirectHandle<Context> context4 =
Utils::OpenDirectHandle(*v8_context3, isolate());
context2->native_context()->set_microtask_queue(isolate(), microtask_queue());
context3->native_context()->set_microtask_queue(isolate(), microtask_queue());
context4->native_context()->set_microtask_queue(isolate(), microtask_queue());
DirectHandle<JSFunction> handler;
DirectHandle<JSProxy> proxy;
DirectHandle<JSProxy> revoked_proxy;
DirectHandle<JSBoundFunction> bound;
// Create a JSFunction on |context2|
{
v8::Context::Scope scope(v8_context2);
handler = RunJS<JSFunction>("()=>{}");
EXPECT_EQ(*context2,
*JSReceiver::GetContextForMicrotask(handler).ToHandleChecked());
}
// Create a JSProxy on |context3|.
{
v8::Context::Scope scope(v8_context3);
ASSERT_TRUE(
v8_context3->Global()
->Set(v8_context3, NewString("handler"), Utils::ToLocal(handler))
.FromJust());
proxy = RunJS<JSProxy>("new Proxy(handler, {})");
revoked_proxy = RunJS<JSProxy>(
"let {proxy, revoke} = Proxy.revocable(handler, {});"
"revoke();"
"proxy");
EXPECT_EQ(*context2,
*JSReceiver::GetContextForMicrotask(proxy).ToHandleChecked());
EXPECT_TRUE(JSReceiver::GetContextForMicrotask(revoked_proxy).is_null());
}
// Create a JSBoundFunction on |context4|.
// Note that its CreationContext and ContextForTaskCancellation is |context2|.
{
v8::Context::Scope scope(v8_context4);
ASSERT_TRUE(
v8_context4->Global()
->Set(v8_context4, NewString("handler"), Utils::ToLocal(handler))
.FromJust());
bound = RunJS<JSBoundFunction>("handler.bind()");
EXPECT_EQ(*context2,
*JSReceiver::GetContextForMicrotask(bound).ToHandleChecked());
}
// Give the objects to the main context.
SetGlobalProperty("handler", Utils::ToLocal(handler));
SetGlobalProperty("proxy", Utils::ToLocal(proxy));
SetGlobalProperty("revoked_proxy", Utils::ToLocal(revoked_proxy));
SetGlobalProperty("bound", Utils::ToLocal(Cast<JSReceiver>(bound)));
RunJS(
"Promise.resolve().then(handler);"
"Promise.reject().catch(proxy);"
"Promise.resolve().then(revoked_proxy);"
"Promise.resolve().then(bound);");
ASSERT_EQ(4, microtask_queue()->size());
DirectHandle<Microtask> microtask1(microtask_queue()->get(0), isolate());
ASSERT_TRUE(IsPromiseFulfillReactionJobTask(*microtask1));
EXPECT_EQ(*context2,
Cast<PromiseFulfillReactionJobTask>(microtask1)->context());
DirectHandle<Microtask> microtask2(microtask_queue()->get(1), isolate());
ASSERT_TRUE(IsPromiseRejectReactionJobTask(*microtask2));
EXPECT_EQ(*context2,
Cast<PromiseRejectReactionJobTask>(microtask2)->context());
DirectHandle<Microtask> microtask3(microtask_queue()->get(2), isolate());
ASSERT_TRUE(IsPromiseFulfillReactionJobTask(*microtask3));
// |microtask3| corresponds to a PromiseReaction for |revoked_proxy|.
// As |revoked_proxy| doesn't have a context, the current context should be
// used as the fallback context.
EXPECT_EQ(*native_context(),
Cast<PromiseFulfillReactionJobTask>(microtask3)->context());
DirectHandle<Microtask> microtask4(microtask_queue()->get(3), isolate());
ASSERT_TRUE(IsPromiseFulfillReactionJobTask(*microtask4));
EXPECT_EQ(*context2,
Cast<PromiseFulfillReactionJobTask>(microtask4)->context());
v8_context4->DetachGlobal();
v8_context3->DetachGlobal();
v8_context2->DetachGlobal();
}
TEST_P(MicrotaskQueueTest, DetachGlobal_Enqueue) {
EXPECT_EQ(0, microtask_queue()->size());
// Detach MicrotaskQueue from the current context.
context()->DetachGlobal();
// No microtask should be enqueued after DetachGlobal call.
EXPECT_EQ(0, microtask_queue()->size());
RunJS("Promise.resolve().then(()=>{})");
EXPECT_EQ(0, microtask_queue()->size());
}
TEST_P(MicrotaskQueueTest, DetachGlobal_Run) {
microtask_queue()->set_microtasks_policy(MicrotasksPolicy::kExplicit);
EXPECT_EQ(0, microtask_queue()->size());
// Enqueue microtasks to the current context.
DirectHandle<JSArray> ran = RunJS<JSArray>(
"var ran = [false, false, false, false];"
"Promise.resolve().then(() => { ran[0] = true; });"
"Promise.reject().catch(() => { ran[1] = true; });"
"ran");
DirectHandle<JSFunction> function =
RunJS<JSFunction>("(function() { ran[2] = true; })");
DirectHandle<CallableTask> callable =
factory()->NewCallableTask(function, Utils::OpenDirectHandle(*context()));
microtask_queue()->EnqueueMicrotask(*callable);
// The handler should not run at this point.
const int kNumExpectedTasks = 3;
for (int i = 0; i < kNumExpectedTasks; ++i) {
EXPECT_TRUE(
IsFalse(*Object::GetElement(isolate(), ran, i).ToHandleChecked()));
}
EXPECT_EQ(kNumExpectedTasks, microtask_queue()->size());
// Detach MicrotaskQueue from the current context.
context()->DetachGlobal();
// RunMicrotasks processes pending Microtasks, but Microtasks that are
// associated to a detached context should be cancelled and should not take
// effect.
microtask_queue()->RunMicrotasks(isolate());
EXPECT_EQ(0, microtask_queue()->size());
for (int i = 0; i < kNumExpectedTasks; ++i) {
EXPECT_TRUE(
IsFalse(*Object::GetElement(isolate(), ran, i).ToHandleChecked()));
}
}
TEST_P(MicrotaskQueueTest, DetachGlobal_PromiseResolveThenableJobTask) {
microtask_queue()->set_microtasks_policy(MicrotasksPolicy::kExplicit);
RunJS(
"var resolve;"
"var promise = new Promise(r => { resolve = r; });"
"promise.then(() => {});"
"resolve({});");
// A PromiseResolveThenableJobTask is pending in the MicrotaskQueue.
EXPECT_EQ(1, microtask_queue()->size());
// Detach MicrotaskQueue from the current context.
context()->DetachGlobal();
// RunMicrotasks processes the pending Microtask, but Microtasks that are
// associated to a detached context should be cancelled and should not take
// effect.
// As PromiseResolveThenableJobTask queues another task for resolution,
// the return value is 2 if it ran.
EXPECT_EQ(1, microtask_queue()->RunMicrotasks(isolate()));
EXPECT_EQ(0, microtask_queue()->size());
}
TEST_P(MicrotaskQueueTest, DetachGlobal_ResolveThenableForeignThen) {
microtask_queue()->set_microtasks_policy(MicrotasksPolicy::kExplicit);
DirectHandle<JSArray> result = RunJS<JSArray>(
"let result = [false];"
"result");
DirectHandle<JSFunction> then =
RunJS<JSFunction>("() => { result[0] = true; }");
DirectHandle<JSPromise> stale_promise;
{
// Create a context with its own microtask queue.
std::unique_ptr<MicrotaskQueue> sub_microtask_queue =
MicrotaskQueue::New(isolate());
sub_microtask_queue->set_microtasks_policy(MicrotasksPolicy::kExplicit);
Local<v8::Context> sub_context = v8::Context::New(
v8_isolate(),
/* extensions= */ nullptr,
/* global_template= */ MaybeLocal<ObjectTemplate>(),
/* global_object= */ MaybeLocal<Value>(),
/* internal_fields_deserializer= */ DeserializeInternalFieldsCallback(),
sub_microtask_queue.get());
{
v8::Context::Scope scope(sub_context);
CHECK(sub_context->Global()
->Set(sub_context, NewString("then"),
Utils::ToLocal(Cast<JSReceiver>(then)))
.FromJust());
ASSERT_EQ(0, microtask_queue()->size());
ASSERT_EQ(0, sub_microtask_queue->size());
ASSERT_TRUE(
IsFalse(*Object::GetElement(isolate(), result, 0).ToHandleChecked()));
// With a regular thenable, a microtask is queued on the sub-context.
RunJS<JSPromise>("Promise.resolve({ then: cb => cb(1) })");
EXPECT_EQ(0, microtask_queue()->size());
EXPECT_EQ(1, sub_microtask_queue->size());
EXPECT_TRUE(
IsFalse(*Object::GetElement(isolate(), result, 0).ToHandleChecked()));
// But when the `then` method comes from another context, a microtask is
// instead queued on the main context.
stale_promise = RunJS<JSPromise>("Promise.resolve({ then })");
EXPECT_EQ(1, microtask_queue()->size());
EXPECT_EQ(1, sub_microtask_queue->size());
EXPECT_TRUE(
IsFalse(*Object::GetElement(isolate(), result, 0).ToHandleChecked()));
}
sub_context->DetachGlobal();
}
EXPECT_EQ(1, microtask_queue()->size());
EXPECT_TRUE(
IsFalse(*Object::GetElement(isolate(), result, 0).ToHandleChecked()));
EXPECT_EQ(1, microtask_queue()->RunMicrotasks(isolate()));
EXPECT_EQ(0, microtask_queue()->size());
EXPECT_TRUE(
IsTrue(*Object::GetElement(isolate(), result, 0).ToHandleChecked()));
}
TEST_P(MicrotaskQueueTest, DetachGlobal_HandlerContext) {
// EnqueueMicrotask should use the context associated to the handler instead
// of the current context. E.g.
// // At Context A.
// let resolved = Promise.resolve();
// // Call DetachGlobal on A, so that microtasks associated to A is
// // cancelled.
//
// // At Context B.
// let handler = () => {
// console.log("here");
// };
// // The microtask to run |handler| should be associated to B instead of A,
// // so that handler runs even |resolved| is on the detached context A.
// resolved.then(handler);
DirectHandle<JSReceiver> results =
isolate()->factory()->NewJSObjectWithNullProto();
// These belong to a stale Context.
DirectHandle<JSPromise> stale_resolved_promise;
DirectHandle<JSPromise> stale_rejected_promise;
DirectHandle<JSReceiver> stale_handler;
Local<v8::Context> sub_context = v8::Context::New(v8_isolate());
{
v8::Context::Scope scope(sub_context);
stale_resolved_promise = RunJS<JSPromise>("Promise.resolve()");
stale_rejected_promise = RunJS<JSPromise>("Promise.reject()");
stale_handler = RunJS<JSReceiver>(
"(results, label) => {"
" results[label] = true;"
"}");
}
// DetachGlobal() cancels all microtasks associated to the context.
sub_context->DetachGlobal();
sub_context.Clear();
SetGlobalProperty("results", Utils::ToLocal(results));
SetGlobalProperty("stale_resolved_promise",
Utils::ToLocal(Cast<JSReceiver>(stale_resolved_promise)));
SetGlobalProperty("stale_rejected_promise",
Utils::ToLocal(Cast<JSReceiver>(stale_rejected_promise)));
SetGlobalProperty("stale_handler", Utils::ToLocal(stale_handler));
// Set valid handlers to stale promises.
RunJS(
"stale_resolved_promise.then(() => {"
" results['stale_resolved_promise'] = true;"
"})");
RunJS(
"stale_rejected_promise.catch(() => {"
" results['stale_rejected_promise'] = true;"
"})");
microtask_queue()->RunMicrotasks(isolate());
EXPECT_TRUE(JSReceiver::HasProperty(isolate(), results,
NameFromChars("stale_resolved_promise"))
.FromJust());
EXPECT_TRUE(JSReceiver::HasProperty(isolate(), results,
NameFromChars("stale_rejected_promise"))
.FromJust());
// Set stale handlers to valid promises.
RunJS(
"Promise.resolve("
" stale_handler.bind(null, results, 'stale_handler_resolve'))");
RunJS(
"Promise.reject("
" stale_handler.bind(null, results, 'stale_handler_reject'))");
microtask_queue()->RunMicrotasks(isolate());
EXPECT_FALSE(JSReceiver::HasProperty(isolate(), results,
NameFromChars("stale_handler_resolve"))
.FromJust());
EXPECT_FALSE(JSReceiver::HasProperty(isolate(), results,
NameFromChars("stale_handler_reject"))
.FromJust());
}
TEST_P(MicrotaskQueueTest, DetachGlobal_Chain) {
DirectHandle<JSPromise> stale_rejected_promise;
Local<v8::Context> sub_context = v8::Context::New(v8_isolate());
{
v8::Context::Scope scope(sub_context);
stale_rejected_promise = RunJS<JSPromise>("Promise.reject()");
}
sub_context->DetachGlobal();
sub_context.Clear();
SetGlobalProperty("stale_rejected_promise",
Utils::ToLocal(Cast<JSReceiver>(stale_rejected_promise)));
DirectHandle<JSArray> result = RunJS<JSArray>(
"let result = [false];"
"stale_rejected_promise"
" .then(() => {})"
" .catch(() => {"
" result[0] = true;"
" });"
"result");
microtask_queue()->RunMicrotasks(isolate());
EXPECT_TRUE(
IsTrue(*Object::GetElement(isolate(), result, 0).ToHandleChecked()));
}
TEST_P(MicrotaskQueueTest, DetachGlobal_InactiveHandler) {
Local<v8::Context> sub_context = v8::Context::New(v8_isolate());
Utils::OpenDirectHandle(*sub_context)
->native_context()
->set_microtask_queue(isolate(), microtask_queue());
DirectHandle<JSArray> result;
DirectHandle<JSFunction> stale_handler;
DirectHandle<JSPromise> stale_promise;
{
v8::Context::Scope scope(sub_context);
result = RunJS<JSArray>("var result = [false, false]; result");
stale_handler = RunJS<JSFunction>("() => { result[0] = true; }");
stale_promise = RunJS<JSPromise>(
"var stale_promise = new Promise(()=>{});"
"stale_promise");
RunJS("stale_promise.then(() => { result [1] = true; });");
}
sub_context->DetachGlobal();
sub_context.Clear();
// The context of |stale_handler| and |stale_promise| is detached at this
// point.
// Ensure that resolution handling for |stale_handler| is cancelled without
// crash. Also, the resolution of |stale_promise| is also cancelled.
SetGlobalProperty("stale_handler", Utils::ToLocal(stale_handler));
RunJS("%EnqueueMicrotask(stale_handler)");
v8_isolate()->EnqueueMicrotask(Utils::ToLocal(stale_handler));
JSPromise::Fulfill(
stale_promise,
direct_handle(ReadOnlyRoots(isolate()).undefined_value(), isolate()));
microtask_queue()->RunMicrotasks(isolate());
EXPECT_TRUE(
IsFalse(*Object::GetElement(isolate(), result, 0).ToHandleChecked()));
EXPECT_TRUE(
IsFalse(*Object::GetElement(isolate(), result, 1).ToHandleChecked()));
}
TEST_P(MicrotaskQueueTest, MicrotasksScope) {
ASSERT_NE(isolate()->default_microtask_queue(), microtask_queue());
microtask_queue()->set_microtasks_policy(MicrotasksPolicy::kScoped);
bool ran = false;
{
MicrotasksScope scope(v8_isolate(), microtask_queue(),
MicrotasksScope::kRunMicrotasks);
microtask_queue()->EnqueueMicrotask(*NewMicrotask([&ran]() {
EXPECT_FALSE(ran);
ran = true;
}));
}
EXPECT_TRUE(ran);
}
INSTANTIATE_TEST_SUITE_P(
, MicrotaskQueueTest, ::testing::Values(false, true),
[](const ::testing::TestParamInfo<MicrotaskQueueTest::ParamType>& info) {
return info.param ? "runtime" : "builtin";
});
} // namespace internal
} // namespace v8

View File

@ -0,0 +1,234 @@
// Copyright 2019 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.
#include <optional>
#include "src/codegen/arm64/decoder-arm64-inl.h"
#include "src/execution/arm64/simulator-arm64.h"
#include "src/execution/pointer-authentication.h"
#include "test/unittests/test-utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#ifdef V8_OS_LINUX
#include <sys/prctl.h> // for prctl
#ifndef PR_PAC_APIBKEY
#define PR_PAC_APIBKEY (1UL << 1)
#endif
#ifndef PR_PAC_GET_ENABLED_KEYS
#define PR_PAC_GET_ENABLED_KEYS 61
#endif
#endif
namespace v8 {
namespace internal {
namespace {
Address SignPCForTesting(Isolate* isolate, Address pc, Address sp) {
if constexpr (!ENABLE_CONTROL_FLOW_INTEGRITY_BOOL) return pc;
#ifdef USE_SIMULATOR
pc = Simulator::AddPAC(pc, sp, Simulator::kPACKeyIB,
Simulator::kInstructionPointer);
#else
asm volatile(
" mov x17, %[pc]\n"
" mov x16, %[sp]\n"
" pacib1716\n"
" mov %[pc], x17\n"
: [pc] "+r"(pc)
: [sp] "r"(sp)
: "x16", "x17");
#endif
return pc;
}
void FunctionToUseForPointerAuthentication() { PrintF("hello, "); }
void AlternativeFunctionToUseForPointerAuthentication() { PrintF("world\n"); }
Address GetRawCodeAddress() {
return PointerAuthentication::StripPAC(
reinterpret_cast<Address>(&FunctionToUseForPointerAuthentication));
}
Address GetAlternativeRawCodeAddress() {
return PointerAuthentication::StripPAC(reinterpret_cast<Address>(
&AlternativeFunctionToUseForPointerAuthentication));
}
// If the platform supports it, corrupt the PAC of |address| so that
// authenticating it will cause a crash.
std::optional<Address> CorruptPACIfSupported(Isolate* isolate,
Address address) {
if constexpr (!ENABLE_CONTROL_FLOW_INTEGRITY_BOOL) return std::nullopt;
// First, find where in an address the PAC is located.
// Produce a valid user address with all bits sets. This way, stripping the
// PAC from the address will reveal which bits are used for it. We need to
// clear the TTBR bit, as it differentiates between user and kernel addresses.
Address user_address_all_ones = 0xffff'ffff'ffff'ffff & ~kTTBRMask;
Address pac_mask = PointerAuthentication::StripPAC(user_address_all_ones) ^
user_address_all_ones;
// If the PAC bits are zero then StripPAC() was a no-op. This means pointer
// authentication isn't supported.
if (pac_mask == 0) {
return std::nullopt;
}
// At this point, pointer authentication is supported, but individual keys
// may still be disabled by the OS. We check that it's enabled, to ensure
// that corrupting the PAC will result in a crash.
#if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_ARM64)
// On Linux 5.13 and later, we can ask the OS what keys are enabled directly.
int enabled_keys = prctl(PR_PAC_GET_ENABLED_KEYS, 0, 0, 0, 0);
if ((enabled_keys != -1) &&
((enabled_keys & PR_PAC_APIBKEY) == PR_PAC_APIBKEY)) {
return address ^ pac_mask;
}
#endif
// Do a "best-effort" check to see if PAC is enabled, by signing values on the
// stack and check if bits are set in the PAC range. We do this check a few
// times because 0 is a valid PAC.
Address stack[] = {0xa, 0xb, 0xc, 0xd};
for (int slot = 0; slot < 4; slot++) {
stack[slot] = SignPCForTesting(isolate, stack[slot],
reinterpret_cast<Address>(&stack[slot]));
if ((stack[slot] & pac_mask) != 0) return address ^ pac_mask;
}
// None of the slots were signed with a non-zero PAC, assume this means PAC
// isn't enabled.
return std::nullopt;
}
} // namespace
using PointerAuthArm64Test = TestWithIsolate;
TEST_F(PointerAuthArm64Test, AuthenticatePC) {
Address pc = GetRawCodeAddress();
Address stack = 0;
stack = SignPCForTesting(i_isolate(), pc, reinterpret_cast<Address>(&stack));
pc = PointerAuthentication::AuthenticatePC(&stack, 0);
EXPECT_EQ(pc, GetRawCodeAddress());
if (auto corrupted_stack = CorruptPACIfSupported(i_isolate(), stack)) {
stack = *corrupted_stack;
EXPECT_DEATH_IF_SUPPORTED(PointerAuthentication::AuthenticatePC(&stack, 0),
"");
}
}
TEST_F(PointerAuthArm64Test, ReplacePC) {
Address pc = GetRawCodeAddress();
Address stack = 0;
stack = SignPCForTesting(i_isolate(), pc, reinterpret_cast<Address>(&stack));
PointerAuthentication::ReplacePC(&stack, GetAlternativeRawCodeAddress(), 0);
if (auto corrupted_stack = CorruptPACIfSupported(i_isolate(), stack)) {
stack = *corrupted_stack;
EXPECT_DEATH_IF_SUPPORTED(PointerAuthentication::ReplacePC(
&stack, GetAlternativeRawCodeAddress(), 0),
"");
}
}
TEST_F(PointerAuthArm64Test, ReplacePCAfterGC) {
v8::PageAllocator* page_allocator = v8::internal::GetPlatformPageAllocator();
size_t page_size = v8::internal::AllocatePageSize();
// Allocate a page and mark it inaccessible, to simulate a code address to a
// page that was reclaimed after a GC.
Address pc = reinterpret_cast<Address>(v8::internal::AllocatePages(
page_allocator, page_allocator->GetRandomMmapAddr(), page_size, page_size,
PageAllocator::Permission::kReadWrite));
CHECK(SetPermissions(page_allocator, pc, page_size,
PageAllocator::Permission::kNoAccess));
// Replacing the signed PC on the stack should work even when the previous PC
// points to an inaccessible page.
Address stack = 0;
stack = SignPCForTesting(i_isolate(), pc, reinterpret_cast<Address>(&stack));
PointerAuthentication::ReplacePC(&stack, GetRawCodeAddress(), 0);
}
#ifdef USE_SIMULATOR
TEST_F(PointerAuthArm64Test, SimulatorComputePAC) {
Decoder<DispatchingDecoderVisitor>* decoder =
new Decoder<DispatchingDecoderVisitor>();
Simulator simulator(decoder);
uint64_t data1 = 0xfb623599da6e8127;
uint64_t data2 = 0x27979fadf7d53cb7;
uint64_t context = 0x477d469dec0b8762;
Simulator::PACKey key = {0x84be85ce9804e94b, 0xec2802d4e0a488e9, -1};
uint64_t pac1 = simulator.ComputePAC(data1, context, key);
uint64_t pac2 = simulator.ComputePAC(data2, context, key);
// NOTE: If the PAC implementation is changed, this may fail due to a hash
// collision.
CHECK_NE(pac1, pac2);
}
TEST_F(PointerAuthArm64Test, SimulatorAddAndAuthPAC) {
i::v8_flags.sim_abort_on_bad_auth = false;
Decoder<DispatchingDecoderVisitor>* decoder =
new Decoder<DispatchingDecoderVisitor>();
Simulator simulator(decoder);
uint64_t ptr = 0x0000000012345678;
uint64_t context = 0x477d469dec0b8762;
Simulator::PACKey key_a = {0x84be85ce9804e94b, 0xec2802d4e0a488e9, 0};
Simulator::PACKey key_b = {0xec1119e288704d13, 0xd7f6b76e1cea585e, 1};
uint64_t ptr_a =
simulator.AddPAC(ptr, context, key_a, Simulator::kInstructionPointer);
// Attempt to authenticate the pointer with PAC using different keys.
uint64_t success =
simulator.AuthPAC(ptr_a, context, key_a, Simulator::kInstructionPointer);
uint64_t fail =
simulator.AuthPAC(ptr_a, context, key_b, Simulator::kInstructionPointer);
uint64_t pac_mask =
simulator.CalculatePACMask(ptr, Simulator::kInstructionPointer, 0);
// NOTE: If the PAC implementation is changed, this may fail due to a hash
// collision.
CHECK_NE((ptr_a & pac_mask), 0);
CHECK_EQ(success, ptr);
CHECK_NE(fail, ptr);
}
TEST_F(PointerAuthArm64Test, SimulatorAddAndStripPAC) {
Decoder<DispatchingDecoderVisitor>* decoder =
new Decoder<DispatchingDecoderVisitor>();
Simulator simulator(decoder);
uint64_t ptr = 0xff00000012345678;
uint64_t pac_mask =
simulator.CalculatePACMask(ptr, Simulator::kInstructionPointer, 0);
uint64_t ptr_a = ptr | pac_mask;
CHECK_EQ(simulator.StripPAC(ptr_a, Simulator::kInstructionPointer), ptr);
}
#endif // USE_SIMULATOR
} // namespace internal
} // namespace v8

View File

@ -0,0 +1,827 @@
// Copyright 2009 the V8 project authors. All rights reserved.
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials provided
// with the distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "include/v8-function.h"
#include "include/v8-locker.h"
#include "src/api/api-inl.h"
#include "src/base/platform/platform.h"
#include "src/debug/debug-interface.h"
#include "src/execution/interrupts-scope.h"
#include "src/execution/isolate.h"
#include "src/init/v8.h"
#include "src/objects/objects-inl.h"
#include "test/unittests/test-utils.h"
#include "testing/gmock-support.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace v8 {
base::Semaphore* semaphore = nullptr;
class TerminatorThread : public base::Thread {
public:
explicit TerminatorThread(i::Isolate* isolate)
: Thread(Options("TerminatorThread")),
isolate_(reinterpret_cast<Isolate*>(isolate)) {}
void Run() override {
semaphore->Wait();
// We need this because IsExecutionTerminating accesses the main pointer
// cage base through ReadOnlyRoots::address_at.
i::PtrComprCageAccessScope ptr_compr_cage_access_scope(
reinterpret_cast<i::Isolate*>(isolate_));
CHECK(!isolate_->IsExecutionTerminating());
isolate_->TerminateExecution();
}
private:
Isolate* isolate_;
};
void Signal(const FunctionCallbackInfo<Value>& info) {
CHECK(i::ValidateCallbackInfo(info));
semaphore->Signal();
}
MaybeLocal<Value> CompileRun(Local<Context> context, Local<String> source) {
Local<Script> script = Script::Compile(context, source).ToLocalChecked();
return script->Run(context);
}
MaybeLocal<Value> CompileRun(Local<Context> context, const char* source) {
return CompileRun(
context,
String::NewFromUtf8(context->GetIsolate(), source).ToLocalChecked());
}
void DoLoop(const FunctionCallbackInfo<Value>& info) {
CHECK(i::ValidateCallbackInfo(info));
TryCatch try_catch(info.GetIsolate());
CHECK(!info.GetIsolate()->IsExecutionTerminating());
MaybeLocal<Value> result = CompileRun(info.GetIsolate()->GetCurrentContext(),
"function f() {"
" var term = true;"
" try {"
" while(true) {"
" if (term) terminate();"
" term = false;"
" }"
" fail();"
" } catch(e) {"
" fail();"
" }"
"}"
"f()");
CHECK(result.IsEmpty());
CHECK(try_catch.HasCaught());
CHECK(try_catch.Exception()->IsNull());
CHECK(try_catch.Message().IsEmpty());
CHECK(!try_catch.CanContinue());
CHECK(info.GetIsolate()->IsExecutionTerminating());
}
void Fail(const FunctionCallbackInfo<Value>& info) { UNREACHABLE(); }
void Loop(const FunctionCallbackInfo<Value>& info) {
CHECK(i::ValidateCallbackInfo(info));
CHECK(!info.GetIsolate()->IsExecutionTerminating());
MaybeLocal<Value> result =
CompileRun(info.GetIsolate()->GetCurrentContext(),
"try { doloop(); fail(); } catch(e) { fail(); }");
CHECK(result.IsEmpty());
CHECK(info.GetIsolate()->IsExecutionTerminating());
}
void TerminateCurrentThread(const FunctionCallbackInfo<Value>& info) {
CHECK(i::ValidateCallbackInfo(info));
CHECK(!info.GetIsolate()->IsExecutionTerminating());
info.GetIsolate()->TerminateExecution();
}
class ThreadTerminationTest : public TestWithIsolate {
public:
void TestTerminatingFromOtherThread(const char* source) {
semaphore = new base::Semaphore(0);
TerminatorThread thread(i_isolate());
CHECK(thread.Start());
HandleScope scope(isolate());
Local<ObjectTemplate> global =
CreateGlobalTemplate(isolate(), Signal, DoLoop);
Local<Context> context = Context::New(isolate(), nullptr, global);
Context::Scope context_scope(context);
CHECK(!isolate()->IsExecutionTerminating());
MaybeLocal<Value> result = TryRunJS(source);
CHECK(result.IsEmpty());
thread.Join();
delete semaphore;
semaphore = nullptr;
}
void TestTerminatingFromCurrentThread(const char* source) {
HandleScope scope(isolate());
Local<ObjectTemplate> global =
CreateGlobalTemplate(isolate(), TerminateCurrentThread, DoLoop);
Local<Context> context = Context::New(isolate(), nullptr, global);
Context::Scope context_scope(context);
CHECK(!isolate()->IsExecutionTerminating());
MaybeLocal<Value> result = TryRunJS(source);
CHECK(result.IsEmpty());
}
Local<ObjectTemplate> CreateGlobalTemplate(Isolate* isolate,
FunctionCallback terminate,
FunctionCallback doloop) {
Local<ObjectTemplate> global = ObjectTemplate::New(isolate);
global->Set(NewString("terminate"),
FunctionTemplate::New(isolate, terminate));
global->Set(NewString("fail"), FunctionTemplate::New(isolate, Fail));
global->Set(NewString("loop"), FunctionTemplate::New(isolate, Loop));
global->Set(NewString("doloop"), FunctionTemplate::New(isolate, doloop));
return global;
}
};
void DoLoopNoCall(const FunctionCallbackInfo<Value>& info) {
CHECK(i::ValidateCallbackInfo(info));
TryCatch try_catch(info.GetIsolate());
CHECK(!info.GetIsolate()->IsExecutionTerminating());
MaybeLocal<Value> result = CompileRun(info.GetIsolate()->GetCurrentContext(),
"var term = true;"
"while(true) {"
" if (term) terminate();"
" term = false;"
"}");
CHECK(result.IsEmpty());
CHECK(try_catch.HasCaught());
CHECK(try_catch.Exception()->IsNull());
CHECK(try_catch.Message().IsEmpty());
CHECK(!try_catch.CanContinue());
CHECK(info.GetIsolate()->IsExecutionTerminating());
}
// Test that a single thread of JavaScript execution can terminate
// itself.
TEST_F(ThreadTerminationTest, TerminateOnlyV8ThreadFromThreadItself) {
HandleScope scope(isolate());
Local<ObjectTemplate> global =
CreateGlobalTemplate(isolate(), TerminateCurrentThread, DoLoop);
Local<Context> context = Context::New(isolate(), nullptr, global);
Context::Scope context_scope(context);
CHECK(!isolate()->IsExecutionTerminating());
// Run a loop that will be infinite if thread termination does not work.
MaybeLocal<Value> result =
TryRunJS("try { loop(); fail(); } catch(e) { fail(); }");
CHECK(result.IsEmpty());
// Test that we can run the code again after thread termination.
CHECK(!isolate()->IsExecutionTerminating());
result = TryRunJS("try { loop(); fail(); } catch(e) { fail(); }");
CHECK(result.IsEmpty());
}
// Test that a single thread of JavaScript execution can terminate
// itself in a loop that performs no calls.
TEST_F(ThreadTerminationTest, TerminateOnlyV8ThreadFromThreadItselfNoLoop) {
HandleScope scope(isolate());
Local<ObjectTemplate> global =
CreateGlobalTemplate(isolate(), TerminateCurrentThread, DoLoopNoCall);
Local<Context> context = Context::New(isolate(), nullptr, global);
Context::Scope context_scope(context);
CHECK(!isolate()->IsExecutionTerminating());
// Run a loop that will be infinite if thread termination does not work.
static const char* source = "try { loop(); fail(); } catch(e) { fail(); }";
MaybeLocal<Value> result = TryRunJS(source);
CHECK(result.IsEmpty());
CHECK(!isolate()->IsExecutionTerminating());
// Test that we can run the code again after thread termination.
result = TryRunJS(source);
CHECK(result.IsEmpty());
}
// Test that a single thread of JavaScript execution can be terminated
// from the side by another thread.
TEST_F(ThreadTerminationTest, TerminateOnlyV8ThreadFromOtherThread) {
// Run a loop that will be infinite if thread termination does not work.
TestTerminatingFromOtherThread(
"try { loop(); fail(); } catch(e) { fail(); }");
}
// Test that execution can be terminated from within JSON.stringify.
TEST_F(ThreadTerminationTest, TerminateJsonStringify) {
TestTerminatingFromCurrentThread(
"var x = [];"
"x[2**31]=1;"
"terminate();"
"JSON.stringify(x);"
"fail();");
}
TEST_F(ThreadTerminationTest, TerminateBigIntMultiplication) {
TestTerminatingFromCurrentThread(
"terminate();"
"var a = 5n ** 555555n;"
"var b = 3n ** 3333333n;"
"a * b;"
"fail();");
}
TEST_F(ThreadTerminationTest, TerminateOptimizedBigIntMultiplication) {
i::v8_flags.allow_natives_syntax = true;
TestTerminatingFromCurrentThread(
"function foo(a, b) { return a * b; }"
"%PrepareFunctionForOptimization(foo);"
"foo(1n, 2n);"
"foo(1n, 2n);"
"%OptimizeFunctionOnNextCall(foo);"
"foo(1n, 2n);"
"var a = 5n ** 555555n;"
"var b = 3n ** 3333333n;"
"terminate();"
"foo(a, b);"
"fail();");
}
TEST_F(ThreadTerminationTest, TerminateBigIntDivision) {
TestTerminatingFromCurrentThread(
"var a = 2n ** 2222222n;"
"var b = 3n ** 333333n;"
"terminate();"
"a / b;"
"fail();");
}
TEST_F(ThreadTerminationTest, TerminateOptimizedBigIntDivision) {
i::v8_flags.allow_natives_syntax = true;
TestTerminatingFromCurrentThread(
"function foo(a, b) { return a / b; }"
"%PrepareFunctionForOptimization(foo);"
"foo(3n, 2n);"
"foo(3n, 2n);"
"%OptimizeFunctionOnNextCall(foo);"
"foo(3n, 2n);"
"var a = 2n ** 2222222n;"
"var b = 3n ** 333333n;"
"terminate();"
"foo(a, b);"
"fail();");
}
TEST_F(ThreadTerminationTest, TerminateBigIntToString) {
TestTerminatingFromCurrentThread(
"var a = 2n ** 2222222n;"
"terminate();"
"a.toString();"
"fail();");
}
TEST_F(ThreadTerminationTest, TerminateBigIntFromString) {
TestTerminatingFromCurrentThread(
"var a = '12344567890'.repeat(100000);\n"
"terminate();\n"
"BigInt(a);\n"
"fail();\n");
}
void LoopGetProperty(const FunctionCallbackInfo<Value>& info) {
CHECK(i::ValidateCallbackInfo(info));
TryCatch try_catch(info.GetIsolate());
CHECK(!info.GetIsolate()->IsExecutionTerminating());
MaybeLocal<Value> result =
CompileRun(info.GetIsolate()->GetCurrentContext(),
"function f() {"
" try {"
" while(true) {"
" terminate_or_return_object().x;"
" }"
" fail();"
" } catch(e) {"
" (function() {})();" // trigger stack check.
" fail();"
" }"
"}"
"f()");
CHECK(result.IsEmpty());
CHECK(try_catch.HasCaught());
CHECK(try_catch.Exception()->IsNull());
CHECK(try_catch.Message().IsEmpty());
CHECK(!try_catch.CanContinue());
CHECK(info.GetIsolate()->IsExecutionTerminating());
}
int call_count = 0;
void TerminateOrReturnObject(const FunctionCallbackInfo<Value>& info) {
CHECK(i::ValidateCallbackInfo(info));
if (++call_count == 10) {
CHECK(!info.GetIsolate()->IsExecutionTerminating());
info.GetIsolate()->TerminateExecution();
return;
}
Local<Object> result = Object::New(info.GetIsolate());
Local<Context> context = info.GetIsolate()->GetCurrentContext();
Maybe<bool> val = result->Set(
context, String::NewFromUtf8(context->GetIsolate(), "x").ToLocalChecked(),
Integer::New(info.GetIsolate(), 42));
CHECK(val.FromJust());
info.GetReturnValue().Set(result);
}
// Test that we correctly handle termination exceptions if they are
// triggered by the creation of error objects in connection with ICs.
TEST_F(ThreadTerminationTest, TerminateLoadICException) {
HandleScope scope(isolate());
Local<ObjectTemplate> global = ObjectTemplate::New(isolate());
global->Set(NewString("terminate_or_return_object"),
FunctionTemplate::New(isolate(), TerminateOrReturnObject));
global->Set(NewString("fail"), FunctionTemplate::New(isolate(), Fail));
global->Set(NewString("loop"),
FunctionTemplate::New(isolate(), LoopGetProperty));
Local<Context> context = Context::New(isolate(), nullptr, global);
Context::Scope context_scope(context);
CHECK(!isolate()->IsExecutionTerminating());
// Run a loop that will be infinite if thread termination does not work.
static const char* source = "try { loop(); fail(); } catch(e) { fail(); }";
call_count = 0;
MaybeLocal<Value> result = CompileRun(isolate()->GetCurrentContext(), source);
CHECK(result.IsEmpty());
// Test that we can run the code again after thread termination.
CHECK(!isolate()->IsExecutionTerminating());
call_count = 0;
result = CompileRun(isolate()->GetCurrentContext(), source);
CHECK(result.IsEmpty());
}
Persistent<String> reenter_script_1;
Persistent<String> reenter_script_2;
void DoLoopCancelTerminate(const FunctionCallbackInfo<Value>& info) {
CHECK(i::ValidateCallbackInfo(info));
Isolate* isolate = info.GetIsolate();
TryCatch try_catch(isolate);
CHECK(!isolate->IsExecutionTerminating());
MaybeLocal<Value> result = CompileRun(isolate->GetCurrentContext(),
"var term = true;"
"while(true) {"
" if (term) terminate();"
" term = false;"
"}"
"fail();");
CHECK(result.IsEmpty());
CHECK(try_catch.HasCaught());
CHECK(try_catch.Exception()->IsNull());
CHECK(try_catch.Message().IsEmpty());
CHECK(!try_catch.CanContinue());
CHECK(isolate->IsExecutionTerminating());
CHECK(try_catch.HasTerminated());
isolate->CancelTerminateExecution();
CHECK(!isolate->IsExecutionTerminating());
}
// Test that a single thread of JavaScript execution can terminate
// itself and then resume execution.
TEST_F(ThreadTerminationTest, TerminateCancelTerminateFromThreadItself) {
HandleScope scope(isolate());
Local<ObjectTemplate> global = CreateGlobalTemplate(
isolate(), TerminateCurrentThread, DoLoopCancelTerminate);
Local<Context> context = Context::New(isolate(), nullptr, global);
Context::Scope context_scope(context);
CHECK(!isolate()->IsExecutionTerminating());
// Check that execution completed with correct return value.
Local<Value> result =
CompileRun(isolate()->GetCurrentContext(),
"try { doloop(); } catch(e) { fail(); } 'completed';")
.ToLocalChecked();
CHECK(result->Equals(isolate()->GetCurrentContext(), NewString("completed"))
.FromJust());
}
void MicrotaskShouldNotRun(const FunctionCallbackInfo<Value>& info) {
UNREACHABLE();
}
void MicrotaskLoopForever(const FunctionCallbackInfo<Value>& info) {
CHECK(i::ValidateCallbackInfo(info));
Isolate* isolate = info.GetIsolate();
HandleScope scope(isolate);
// Enqueue another should-not-run task to ensure we clean out the queue
// when we terminate.
isolate->EnqueueMicrotask(
Function::New(isolate->GetCurrentContext(), MicrotaskShouldNotRun)
.ToLocalChecked());
CompileRun(isolate->GetCurrentContext(), "terminate(); while (true) { }");
CHECK(isolate->IsExecutionTerminating());
}
TEST_F(ThreadTerminationTest, TerminateFromOtherThreadWhileMicrotaskRunning) {
semaphore = new base::Semaphore(0);
TerminatorThread thread(i_isolate());
CHECK(thread.Start());
isolate()->SetMicrotasksPolicy(MicrotasksPolicy::kExplicit);
HandleScope scope(isolate());
Local<ObjectTemplate> global =
CreateGlobalTemplate(isolate(), Signal, DoLoop);
Local<Context> context = Context::New(isolate(), nullptr, global);
Context::Scope context_scope(context);
isolate()->EnqueueMicrotask(
Function::New(isolate()->GetCurrentContext(), MicrotaskLoopForever)
.ToLocalChecked());
// The second task should never be run because we bail out if we're
// terminating.
isolate()->EnqueueMicrotask(
Function::New(isolate()->GetCurrentContext(), MicrotaskShouldNotRun)
.ToLocalChecked());
isolate()->PerformMicrotaskCheckpoint();
isolate()->CancelTerminateExecution();
// Should not run MicrotaskShouldNotRun.
isolate()->PerformMicrotaskCheckpoint();
thread.Join();
delete semaphore;
semaphore = nullptr;
}
static int callback_counter = 0;
static void CounterCallback(Isolate* isolate, void* data) {
callback_counter++;
}
TEST_F(ThreadTerminationTest, PostponeTerminateException) {
HandleScope scope(isolate());
Local<ObjectTemplate> global =
CreateGlobalTemplate(isolate(), TerminateCurrentThread, DoLoop);
Local<Context> context = Context::New(isolate(), nullptr, global);
Context::Scope context_scope(context);
TryCatch try_catch(isolate());
static const char* terminate_and_loop =
"terminate(); for (var i = 0; i < 10000; i++);";
{ // Postpone terminate execution interrupts.
i::PostponeInterruptsScope p1(i_isolate(),
i::StackGuard::TERMINATE_EXECUTION);
// API interrupts should still be triggered.
isolate()->RequestInterrupt(&CounterCallback, nullptr);
CHECK_EQ(0, callback_counter);
RunJS(terminate_and_loop);
CHECK(!try_catch.HasTerminated());
CHECK_EQ(1, callback_counter);
{ // Postpone API interrupts as well.
i::PostponeInterruptsScope p2(i_isolate(), i::StackGuard::API_INTERRUPT);
// None of the two interrupts should trigger.
isolate()->RequestInterrupt(&CounterCallback, nullptr);
RunJS(terminate_and_loop);
CHECK(!try_catch.HasTerminated());
CHECK_EQ(1, callback_counter);
}
// Now the previously requested API interrupt should trigger.
RunJS(terminate_and_loop);
CHECK(!try_catch.HasTerminated());
CHECK_EQ(2, callback_counter);
}
// Now the previously requested terminate execution interrupt should trigger.
TryRunJS("for (var i = 0; i < 10000; i++);");
CHECK(try_catch.HasTerminated());
CHECK_EQ(2, callback_counter);
}
static void AssertFinishedCodeRun(Isolate* isolate) {
TryCatch try_catch(isolate);
CompileRun(isolate->GetCurrentContext(), "for (var i = 0; i < 10000; i++);");
CHECK(!try_catch.HasTerminated());
}
void RequestTermianteAndCallAPI(const FunctionCallbackInfo<Value>& info) {
CHECK(i::ValidateCallbackInfo(info));
info.GetIsolate()->TerminateExecution();
AssertFinishedCodeRun(info.GetIsolate());
}
TEST_F(ThreadTerminationTest, ErrorObjectAfterTermination) {
HandleScope scope(isolate());
Local<Context> context = Context::New(isolate());
Context::Scope context_scope(context);
isolate()->TerminateExecution();
Local<Value> error = Exception::Error(NewString("error"));
CHECK(error->IsNativeError());
}
void InnerTryCallTerminate(const FunctionCallbackInfo<Value>& info) {
CHECK(i::ValidateCallbackInfo(info));
CHECK(!info.GetIsolate()->IsExecutionTerminating());
Isolate* isolate = info.GetIsolate();
Local<Object> global = isolate->GetCurrentContext()->Global();
Local<Function> loop = Local<Function>::Cast(
global
->Get(isolate->GetCurrentContext(),
String::NewFromUtf8(isolate, "loop").ToLocalChecked())
.ToLocalChecked());
i::MaybeDirectHandle<i::Object> exception;
i::MaybeDirectHandle<i::Object> result = i::Execution::TryCall(
reinterpret_cast<i::Isolate*>(isolate), Utils::OpenDirectHandle((*loop)),
Utils::OpenDirectHandle((*global)), {},
i::Execution::MessageHandling::kReport, &exception);
CHECK(result.is_null());
CHECK(exception.is_null());
// TryCall reschedules the termination exception.
CHECK(info.GetIsolate()->IsExecutionTerminating());
}
TEST_F(ThreadTerminationTest, TerminationInInnerTryCall) {
HandleScope scope(isolate());
Local<ObjectTemplate> global_template =
CreateGlobalTemplate(isolate(), TerminateCurrentThread, DoLoopNoCall);
global_template->Set(NewString("inner_try_call_terminate"),
FunctionTemplate::New(isolate(), InnerTryCallTerminate));
Local<Context> context = Context::New(isolate(), nullptr, global_template);
Context::Scope context_scope(context);
{
TryCatch try_catch(isolate());
TryRunJS("inner_try_call_terminate()");
CHECK(try_catch.HasTerminated());
// Any further exectutions in this TryCatch scope would fail.
CHECK(isolate()->IsExecutionTerminating());
}
// Leaving the TryCatch cleared the termination exception.
Maybe<int32_t> result =
RunJS("2 + 2")->Int32Value(isolate()->GetCurrentContext());
CHECK_EQ(4, result.FromJust());
CHECK(!isolate()->IsExecutionTerminating());
}
TEST_F(ThreadTerminationTest, TerminateAndTryCall) {
i::v8_flags.allow_natives_syntax = true;
HandleScope scope(isolate());
Local<ObjectTemplate> global = CreateGlobalTemplate(
isolate(), TerminateCurrentThread, DoLoopCancelTerminate);
Local<Context> context = Context::New(isolate(), nullptr, global);
Context::Scope context_scope(context);
CHECK(!isolate()->IsExecutionTerminating());
{
TryCatch try_catch(isolate());
CHECK(!isolate()->IsExecutionTerminating());
// Terminate execution has been triggered inside TryCall, but re-requested
// to trigger later.
CHECK(TryRunJS("terminate(); reference_error();").IsEmpty());
CHECK(try_catch.HasCaught());
CHECK(!isolate()->IsExecutionTerminating());
Local<Value> value =
context->Global()
->Get(isolate()->GetCurrentContext(), NewString("terminate"))
.ToLocalChecked();
CHECK(value->IsFunction());
// Any further executions in this TryCatch scope fail.
CHECK(!isolate()->IsExecutionTerminating());
CHECK(TryRunJS("1 + 1").IsEmpty());
CHECK(isolate()->IsExecutionTerminating());
}
// Leaving the TryCatch cleared the termination exception.
Maybe<int32_t> result =
RunJS("2 + 2")->Int32Value(isolate()->GetCurrentContext());
CHECK_EQ(4, result.FromJust());
CHECK(!isolate()->IsExecutionTerminating());
}
class ConsoleImpl : public debug::ConsoleDelegate {
private:
void Log(const debug::ConsoleCallArguments& info,
const debug::ConsoleContext&) override {
CompileRun(Isolate::GetCurrent()->GetCurrentContext(), "1 + 1");
}
};
TEST_F(ThreadTerminationTest, TerminateConsole) {
i::v8_flags.allow_natives_syntax = true;
ConsoleImpl console_impl;
debug::SetConsoleDelegate(isolate(), &console_impl);
HandleScope scope(isolate());
Local<ObjectTemplate> global = CreateGlobalTemplate(
isolate(), TerminateCurrentThread, DoLoopCancelTerminate);
Local<Context> context = Context::New(isolate(), nullptr, global);
Context::Scope context_scope(context);
{
// setup console global.
HandleScope inner_scope(isolate());
Local<String> name = String::NewFromUtf8Literal(
isolate(), "console", NewStringType::kInternalized);
Local<Value> console =
context->GetExtrasBindingObject()->Get(context, name).ToLocalChecked();
context->Global()->Set(context, name, console).FromJust();
}
CHECK(!isolate()->IsExecutionTerminating());
TryCatch try_catch(isolate());
CHECK(!isolate()->IsExecutionTerminating());
CHECK(TryRunJS("terminate(); console.log(); fail();").IsEmpty());
CHECK(try_catch.HasCaught());
CHECK(isolate()->IsExecutionTerminating());
}
TEST_F(ThreadTerminationTest, TerminationClearArrayJoinStack) {
internal::v8_flags.allow_natives_syntax = true;
HandleScope scope(isolate());
Local<ObjectTemplate> global_template =
CreateGlobalTemplate(isolate(), TerminateCurrentThread, DoLoopNoCall);
{
Local<Context> context = Context::New(isolate(), nullptr, global_template);
Context::Scope context_scope(context);
{
TryCatch try_catch(isolate());
TryRunJS(
"var error = false;"
"var a = [{toString(){if(error)loop()}}];"
"function Join(){ return a.join();}; "
"%PrepareFunctionForOptimization(Join);"
"Join();"
"%OptimizeFunctionOnNextCall(Join);"
"error = true;"
"Join();");
CHECK(try_catch.HasTerminated());
CHECK(isolate()->IsExecutionTerminating());
}
EXPECT_THAT(RunJS("a[0] = 1; Join();"), testing::IsString("1"));
}
{
Local<Context> context = Context::New(isolate(), nullptr, global_template);
Context::Scope context_scope(context);
{
TryCatch try_catch(isolate());
TryRunJS(
"var a = [{toString(){loop()}}];"
"function Join(){ return a.join();}; "
"Join();");
CHECK(try_catch.HasTerminated());
CHECK(isolate()->IsExecutionTerminating());
}
EXPECT_THAT(RunJS("a[0] = 1; Join();"), testing::IsString("1"));
}
{
ConsoleImpl console_impl;
debug::SetConsoleDelegate(isolate(), &console_impl);
HandleScope middle_scope(isolate());
Local<Context> context = Context::New(isolate(), nullptr, global_template);
Context::Scope context_scope(context);
{
// setup console global.
HandleScope inner_scope(isolate());
Local<String> name = String::NewFromUtf8Literal(
isolate(), "console", NewStringType::kInternalized);
Local<Value> console = context->GetExtrasBindingObject()
->Get(context, name)
.ToLocalChecked();
context->Global()->Set(context, name, console).FromJust();
}
CHECK(!isolate()->IsExecutionTerminating());
{
TryCatch try_catch(isolate());
CHECK(!isolate()->IsExecutionTerminating());
CHECK(TryRunJS("var a = [{toString(){terminate();console.log();fail()}}];"
"function Join() {return a.join();}"
"Join();")
.IsEmpty());
CHECK(try_catch.HasCaught());
CHECK(isolate()->IsExecutionTerminating());
}
EXPECT_THAT(RunJS("a[0] = 1; Join();"), testing::IsString("1"));
}
}
class TerminatorSleeperThread : public base::Thread {
public:
explicit TerminatorSleeperThread(Isolate* isolate, int sleep_ms)
: Thread(Options("TerminatorSlepperThread")),
isolate_(isolate),
sleep_ms_(sleep_ms) {}
void Run() override {
base::OS::Sleep(base::TimeDelta::FromMilliseconds(sleep_ms_));
isolate_->TerminateExecution();
}
private:
Isolate* isolate_;
int sleep_ms_;
};
TEST_F(ThreadTerminationTest, TerminateRegExp) {
i::v8_flags.allow_natives_syntax = true;
// We want to be stuck regexp execution, so no fallback to linear-time
// engine.
// TODO(mbid,v8:10765): Find a way to test interrupt support of the
// experimental engine.
i::v8_flags.enable_experimental_regexp_engine_on_excessive_backtracks = false;
HandleScope scope(isolate());
Local<ObjectTemplate> global = CreateGlobalTemplate(
isolate(), TerminateCurrentThread, DoLoopCancelTerminate);
Local<Context> context = Context::New(isolate(), nullptr, global);
Context::Scope context_scope(context);
CHECK(!isolate()->IsExecutionTerminating());
TryCatch try_catch(isolate());
CHECK(!isolate()->IsExecutionTerminating());
CHECK(!RunJS("var re = /(x+)+y$/; re.test('x');").IsEmpty());
CHECK(!isolate()->IsExecutionTerminating());
TerminatorSleeperThread terminator(isolate(), 100);
CHECK(terminator.Start());
CHECK(TryRunJS("re.test('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); fail();")
.IsEmpty());
CHECK(try_catch.HasCaught());
CHECK(isolate()->IsExecutionTerminating());
}
TEST_F(ThreadTerminationTest, TerminateInMicrotask) {
Locker locker(isolate());
isolate()->SetMicrotasksPolicy(MicrotasksPolicy::kExplicit);
HandleScope scope(isolate());
Local<ObjectTemplate> global = CreateGlobalTemplate(
isolate(), TerminateCurrentThread, DoLoopCancelTerminate);
Local<Context> context1 = Context::New(isolate(), nullptr, global);
Local<Context> context2 = Context::New(isolate(), nullptr, global);
{
TryCatch try_catch(isolate());
{
Context::Scope context_scope(context1);
CHECK(!isolate()->IsExecutionTerminating());
CHECK(!RunJS("Promise.resolve().then(function() {"
"terminate(); loop(); fail();})")
.IsEmpty());
CHECK(!try_catch.HasCaught());
}
{
Context::Scope context_scope(context2);
CHECK(context2 == isolate()->GetCurrentContext());
CHECK(context2 == isolate()->GetEnteredOrMicrotaskContext());
CHECK(!isolate()->IsExecutionTerminating());
isolate()->PerformMicrotaskCheckpoint();
CHECK(context2 == isolate()->GetCurrentContext());
CHECK(context2 == isolate()->GetEnteredOrMicrotaskContext());
CHECK(try_catch.HasCaught());
CHECK(try_catch.HasTerminated());
// Any further exectutions in this TryCatch scope would fail.
CHECK(isolate()->IsExecutionTerminating());
CHECK(!i_isolate()->stack_guard()->CheckTerminateExecution());
}
}
CHECK(!i_isolate()->stack_guard()->CheckTerminateExecution());
CHECK(!isolate()->IsExecutionTerminating());
}
void TerminationMicrotask(void* data) {
Isolate::GetCurrent()->TerminateExecution();
CompileRun(Isolate::GetCurrent()->GetCurrentContext(), "");
}
void UnreachableMicrotask(void* data) { UNREACHABLE(); }
TEST_F(ThreadTerminationTest, TerminateInApiMicrotask) {
Locker locker(isolate());
isolate()->SetMicrotasksPolicy(MicrotasksPolicy::kExplicit);
HandleScope scope(isolate());
Local<ObjectTemplate> global = CreateGlobalTemplate(
isolate(), TerminateCurrentThread, DoLoopCancelTerminate);
Local<Context> context = Context::New(isolate(), nullptr, global);
{
TryCatch try_catch(isolate());
Context::Scope context_scope(context);
CHECK(!isolate()->IsExecutionTerminating());
isolate()->EnqueueMicrotask(TerminationMicrotask);
isolate()->EnqueueMicrotask(UnreachableMicrotask);
isolate()->PerformMicrotaskCheckpoint();
CHECK(try_catch.HasCaught());
CHECK(try_catch.HasTerminated());
CHECK(isolate()->IsExecutionTerminating());
}
CHECK(!isolate()->IsExecutionTerminating());
}
} // namespace v8

View File

@ -0,0 +1,89 @@
// Copyright 2008 the V8 project authors. All rights reserved.
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials provided
// with the distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "src/execution/thread-id.h"
#include "test/unittests/test-utils.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace v8 {
namespace internal {
using ThreadsTest = TestWithIsolate;
// {ThreadId} must be trivially copyable to be stored in {std::atomic}.
ASSERT_TRIVIALLY_COPYABLE(i::ThreadId);
using AtomicThreadId = std::atomic<i::ThreadId>;
class ThreadIdValidationThread : public base::Thread {
public:
ThreadIdValidationThread(base::Thread* thread_to_start, AtomicThreadId* refs,
unsigned int thread_no, base::Semaphore* semaphore)
: Thread(Options("ThreadRefValidationThread")),
refs_(refs),
thread_no_(thread_no),
thread_to_start_(thread_to_start),
semaphore_(semaphore) {}
void Run() override {
i::ThreadId thread_id = i::ThreadId::Current();
CHECK(thread_id.IsValid());
for (int i = 0; i < thread_no_; i++) {
CHECK_NE(refs_[i].load(std::memory_order_relaxed), thread_id);
}
refs_[thread_no_].store(thread_id, std::memory_order_relaxed);
if (thread_to_start_ != nullptr) {
CHECK(thread_to_start_->Start());
}
semaphore_->Signal();
}
private:
AtomicThreadId* const refs_;
const int thread_no_;
base::Thread* const thread_to_start_;
base::Semaphore* const semaphore_;
};
TEST_F(ThreadsTest, ThreadIdValidation) {
constexpr int kNThreads = 100;
std::unique_ptr<ThreadIdValidationThread> threads[kNThreads];
AtomicThreadId refs[kNThreads];
base::Semaphore semaphore(0);
for (int i = kNThreads - 1; i >= 0; i--) {
ThreadIdValidationThread* prev =
i == kNThreads - 1 ? nullptr : threads[i + 1].get();
threads[i] =
std::make_unique<ThreadIdValidationThread>(prev, refs, i, &semaphore);
}
CHECK(threads[0]->Start());
for (int i = 0; i < kNThreads; i++) {
semaphore.Wait();
}
}
} // namespace internal
} // namespace v8