// 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 "src/api.h" #include "src/isolate.h" #include "src/objects-inl.h" #include "src/v8.h" #include "test/cctest/cctest.h" #include "src/base/platform/platform.h" v8::base::Semaphore* semaphore = nullptr; void Signal(const v8::FunctionCallbackInfo& args) { semaphore->Signal(); } void TerminateCurrentThread(const v8::FunctionCallbackInfo& args) { CHECK(!args.GetIsolate()->IsExecutionTerminating()); args.GetIsolate()->TerminateExecution(); } void Fail(const v8::FunctionCallbackInfo& args) { UNREACHABLE(); } void Loop(const v8::FunctionCallbackInfo& args) { CHECK(!args.GetIsolate()->IsExecutionTerminating()); v8::MaybeLocal result = CompileRun(args.GetIsolate()->GetCurrentContext(), "try { doloop(); fail(); } catch(e) { fail(); }"); CHECK(result.IsEmpty()); CHECK(args.GetIsolate()->IsExecutionTerminating()); } void DoLoop(const v8::FunctionCallbackInfo& args) { v8::TryCatch try_catch(args.GetIsolate()); CHECK(!args.GetIsolate()->IsExecutionTerminating()); v8::MaybeLocal result = CompileRun(args.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(args.GetIsolate()->IsExecutionTerminating()); } void DoLoopNoCall(const v8::FunctionCallbackInfo& args) { v8::TryCatch try_catch(args.GetIsolate()); CHECK(!args.GetIsolate()->IsExecutionTerminating()); v8::MaybeLocal result = CompileRun(args.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(args.GetIsolate()->IsExecutionTerminating()); } v8::Local CreateGlobalTemplate( v8::Isolate* isolate, v8::FunctionCallback terminate, v8::FunctionCallback doloop) { v8::Local global = v8::ObjectTemplate::New(isolate); global->Set(v8_str("terminate"), v8::FunctionTemplate::New(isolate, terminate)); global->Set(v8_str("fail"), v8::FunctionTemplate::New(isolate, Fail)); global->Set(v8_str("loop"), v8::FunctionTemplate::New(isolate, Loop)); global->Set(v8_str("doloop"), v8::FunctionTemplate::New(isolate, doloop)); return global; } // Test that a single thread of JavaScript execution can terminate // itself. TEST(TerminateOnlyV8ThreadFromThreadItself) { v8::HandleScope scope(CcTest::isolate()); v8::Local global = CreateGlobalTemplate(CcTest::isolate(), TerminateCurrentThread, DoLoop); v8::Local context = v8::Context::New(CcTest::isolate(), nullptr, global); v8::Context::Scope context_scope(context); CHECK(!CcTest::isolate()->IsExecutionTerminating()); // Run a loop that will be infinite if thread termination does not work. v8::MaybeLocal result = CompileRun(CcTest::isolate()->GetCurrentContext(), "try { loop(); fail(); } catch(e) { fail(); }"); CHECK(result.IsEmpty()); // Test that we can run the code again after thread termination. CHECK(!CcTest::isolate()->IsExecutionTerminating()); result = CompileRun(CcTest::isolate()->GetCurrentContext(), "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(TerminateOnlyV8ThreadFromThreadItselfNoLoop) { v8::HandleScope scope(CcTest::isolate()); v8::Local global = CreateGlobalTemplate( CcTest::isolate(), TerminateCurrentThread, DoLoopNoCall); v8::Local context = v8::Context::New(CcTest::isolate(), nullptr, global); v8::Context::Scope context_scope(context); CHECK(!CcTest::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(); }"; v8::MaybeLocal result = CompileRun(CcTest::isolate()->GetCurrentContext(), source); CHECK(result.IsEmpty()); CHECK(!CcTest::isolate()->IsExecutionTerminating()); // Test that we can run the code again after thread termination. result = CompileRun(CcTest::isolate()->GetCurrentContext(), source); CHECK(result.IsEmpty()); } class TerminatorThread : public v8::base::Thread { public: explicit TerminatorThread(i::Isolate* isolate) : Thread(Options("TerminatorThread")), isolate_(reinterpret_cast(isolate)) {} void Run() { semaphore->Wait(); CHECK(!isolate_->IsExecutionTerminating()); isolate_->TerminateExecution(); } private: v8::Isolate* isolate_; }; // Test that a single thread of JavaScript execution can be terminated // from the side by another thread. TEST(TerminateOnlyV8ThreadFromOtherThread) { semaphore = new v8::base::Semaphore(0); TerminatorThread thread(CcTest::i_isolate()); thread.Start(); v8::HandleScope scope(CcTest::isolate()); v8::Local global = CreateGlobalTemplate(CcTest::isolate(), Signal, DoLoop); v8::Local context = v8::Context::New(CcTest::isolate(), nullptr, global); v8::Context::Scope context_scope(context); CHECK(!CcTest::isolate()->IsExecutionTerminating()); // Run a loop that will be infinite if thread termination does not work. v8::MaybeLocal result = CompileRun(CcTest::isolate()->GetCurrentContext(), "try { loop(); fail(); } catch(e) { fail(); }"); CHECK(result.IsEmpty()); thread.Join(); delete semaphore; semaphore = nullptr; } // Test that execution can be terminated from within JSON.stringify. TEST(TerminateJsonStringify) { semaphore = new v8::base::Semaphore(0); TerminatorThread thread(CcTest::i_isolate()); thread.Start(); v8::HandleScope scope(CcTest::isolate()); v8::Local global = CreateGlobalTemplate(CcTest::isolate(), Signal, DoLoop); v8::Local context = v8::Context::New(CcTest::isolate(), nullptr, global); v8::Context::Scope context_scope(context); CHECK(!CcTest::isolate()->IsExecutionTerminating()); v8::MaybeLocal result = CompileRun(CcTest::isolate()->GetCurrentContext(), "var x = [];" "x[2**31]=1;" "terminate();" "JSON.stringify(x);" "fail();"); CHECK(result.IsEmpty()); thread.Join(); delete semaphore; semaphore = nullptr; } int call_count = 0; void TerminateOrReturnObject(const v8::FunctionCallbackInfo& args) { if (++call_count == 10) { CHECK(!args.GetIsolate()->IsExecutionTerminating()); args.GetIsolate()->TerminateExecution(); return; } v8::Local result = v8::Object::New(args.GetIsolate()); v8::Maybe val = result->Set(args.GetIsolate()->GetCurrentContext(), v8_str("x"), v8::Integer::New(args.GetIsolate(), 42)); CHECK(val.FromJust()); args.GetReturnValue().Set(result); } void LoopGetProperty(const v8::FunctionCallbackInfo& args) { v8::TryCatch try_catch(args.GetIsolate()); CHECK(!args.GetIsolate()->IsExecutionTerminating()); v8::MaybeLocal result = CompileRun(args.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(args.GetIsolate()->IsExecutionTerminating()); } // Test that we correctly handle termination exceptions if they are // triggered by the creation of error objects in connection with ICs. TEST(TerminateLoadICException) { v8::Isolate* isolate = CcTest::isolate(); v8::HandleScope scope(isolate); v8::Local global = v8::ObjectTemplate::New(isolate); global->Set(v8_str("terminate_or_return_object"), v8::FunctionTemplate::New(isolate, TerminateOrReturnObject)); global->Set(v8_str("fail"), v8::FunctionTemplate::New(isolate, Fail)); global->Set(v8_str("loop"), v8::FunctionTemplate::New(isolate, LoopGetProperty)); v8::Local context = v8::Context::New(isolate, nullptr, global); v8::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; v8::MaybeLocal 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()); } v8::Persistent reenter_script_1; v8::Persistent reenter_script_2; void ReenterAfterTermination(const v8::FunctionCallbackInfo& args) { v8::TryCatch try_catch(args.GetIsolate()); v8::Isolate* isolate = args.GetIsolate(); CHECK(!isolate->IsExecutionTerminating()); v8::Local script = v8::Local::New(isolate, reenter_script_1); v8::MaybeLocal result = CompileRun(script); CHECK(result.IsEmpty()); CHECK(try_catch.HasCaught()); CHECK(try_catch.Exception()->IsNull()); CHECK(try_catch.Message().IsEmpty()); CHECK(!try_catch.CanContinue()); CHECK(isolate->IsExecutionTerminating()); script = v8::Local::New(isolate, reenter_script_2); v8::MaybeLocal compiled_script = v8::Script::Compile(isolate->GetCurrentContext(), script); CHECK(compiled_script.IsEmpty()); } // Test that reentry into V8 while the termination exception is still pending // (has not yet unwound the 0-level JS frame) does not crash. TEST(TerminateAndReenterFromThreadItself) { v8::Isolate* isolate = CcTest::isolate(); v8::HandleScope scope(isolate); v8::Local global = CreateGlobalTemplate( isolate, TerminateCurrentThread, ReenterAfterTermination); v8::Local context = v8::Context::New(isolate, nullptr, global); v8::Context::Scope context_scope(context); CHECK(!v8::Isolate::GetCurrent()->IsExecutionTerminating()); // Create script strings upfront as it won't work when terminating. reenter_script_1.Reset(isolate, v8_str( "function f() {" " var term = true;" " try {" " while(true) {" " if (term) terminate();" " term = false;" " }" " fail();" " } catch(e) {" " fail();" " }" "}" "f()")); reenter_script_2.Reset(isolate, v8_str("function f() { fail(); } f()")); CompileRun("try { loop(); fail(); } catch(e) { fail(); }"); CHECK(!isolate->IsExecutionTerminating()); // Check we can run JS again after termination. CHECK(CompileRun("function f() { return true; } f()")->IsTrue()); reenter_script_1.Reset(); reenter_script_2.Reset(); } void DoLoopCancelTerminate(const v8::FunctionCallbackInfo& args) { v8::TryCatch try_catch(args.GetIsolate()); CHECK(!v8::Isolate::GetCurrent()->IsExecutionTerminating()); v8::MaybeLocal result = CompileRun(args.GetIsolate()->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(v8::Isolate::GetCurrent()->IsExecutionTerminating()); CHECK(try_catch.HasTerminated()); CcTest::isolate()->CancelTerminateExecution(); CHECK(!v8::Isolate::GetCurrent()->IsExecutionTerminating()); } // Test that a single thread of JavaScript execution can terminate // itself and then resume execution. TEST(TerminateCancelTerminateFromThreadItself) { v8::Isolate* isolate = CcTest::isolate(); v8::HandleScope scope(isolate); v8::Local global = CreateGlobalTemplate( isolate, TerminateCurrentThread, DoLoopCancelTerminate); v8::Local context = v8::Context::New(isolate, nullptr, global); v8::Context::Scope context_scope(context); CHECK(!CcTest::isolate()->IsExecutionTerminating()); // Check that execution completed with correct return value. v8::Local result = CompileRun(isolate->GetCurrentContext(), "try { doloop(); } catch(e) { fail(); } 'completed';") .ToLocalChecked(); CHECK(result->Equals(isolate->GetCurrentContext(), v8_str("completed")) .FromJust()); } void MicrotaskShouldNotRun(const v8::FunctionCallbackInfo& info) { UNREACHABLE(); } void MicrotaskLoopForever(const v8::FunctionCallbackInfo& info) { v8::Isolate* isolate = info.GetIsolate(); v8::HandleScope scope(isolate); // Enqueue another should-not-run task to ensure we clean out the queue // when we terminate. isolate->EnqueueMicrotask( v8::Function::New(isolate->GetCurrentContext(), MicrotaskShouldNotRun) .ToLocalChecked()); CompileRun("terminate(); while (true) { }"); CHECK(v8::Isolate::GetCurrent()->IsExecutionTerminating()); } TEST(TerminateFromOtherThreadWhileMicrotaskRunning) { semaphore = new v8::base::Semaphore(0); TerminatorThread thread(CcTest::i_isolate()); thread.Start(); v8::Isolate* isolate = CcTest::isolate(); isolate->SetMicrotasksPolicy(v8::MicrotasksPolicy::kExplicit); v8::HandleScope scope(isolate); v8::Local global = CreateGlobalTemplate(CcTest::isolate(), Signal, DoLoop); v8::Local context = v8::Context::New(CcTest::isolate(), nullptr, global); v8::Context::Scope context_scope(context); isolate->EnqueueMicrotask( v8::Function::New(isolate->GetCurrentContext(), MicrotaskLoopForever) .ToLocalChecked()); // The second task should never be run because we bail out if we're // terminating. isolate->EnqueueMicrotask( v8::Function::New(isolate->GetCurrentContext(), MicrotaskShouldNotRun) .ToLocalChecked()); isolate->RunMicrotasks(); isolate->CancelTerminateExecution(); isolate->RunMicrotasks(); // should not run MicrotaskShouldNotRun thread.Join(); delete semaphore; semaphore = nullptr; } static int callback_counter = 0; static void CounterCallback(v8::Isolate* isolate, void* data) { callback_counter++; } TEST(PostponeTerminateException) { v8::Isolate* isolate = CcTest::isolate(); v8::HandleScope scope(isolate); v8::Local global = CreateGlobalTemplate(CcTest::isolate(), TerminateCurrentThread, DoLoop); v8::Local context = v8::Context::New(CcTest::isolate(), nullptr, global); v8::Context::Scope context_scope(context); v8::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(CcTest::i_isolate(), i::StackGuard::TERMINATE_EXECUTION); // API interrupts should still be triggered. CcTest::isolate()->RequestInterrupt(&CounterCallback, nullptr); CHECK_EQ(0, callback_counter); CompileRun(terminate_and_loop); CHECK(!try_catch.HasTerminated()); CHECK_EQ(1, callback_counter); { // Postpone API interrupts as well. i::PostponeInterruptsScope p2(CcTest::i_isolate(), i::StackGuard::API_INTERRUPT); // None of the two interrupts should trigger. CcTest::isolate()->RequestInterrupt(&CounterCallback, nullptr); CompileRun(terminate_and_loop); CHECK(!try_catch.HasTerminated()); CHECK_EQ(1, callback_counter); } // Now the previously requested API interrupt should trigger. CompileRun(terminate_and_loop); CHECK(!try_catch.HasTerminated()); CHECK_EQ(2, callback_counter); } // Now the previously requested terminate execution interrupt should trigger. CompileRun("for (var i = 0; i < 10000; i++);"); CHECK(try_catch.HasTerminated()); CHECK_EQ(2, callback_counter); } TEST(ErrorObjectAfterTermination) { v8::Isolate* isolate = CcTest::isolate(); v8::HandleScope scope(isolate); v8::Local context = v8::Context::New(CcTest::isolate()); v8::Context::Scope context_scope(context); isolate->TerminateExecution(); v8::Local error = v8::Exception::Error(v8_str("error")); CHECK(error->IsNativeError()); } void InnerTryCallTerminate(const v8::FunctionCallbackInfo& args) { CHECK(!args.GetIsolate()->IsExecutionTerminating()); v8::Local global = CcTest::global(); v8::Local loop = v8::Local::Cast( global->Get(CcTest::isolate()->GetCurrentContext(), v8_str("loop")) .ToLocalChecked()); i::MaybeHandle exception; i::MaybeHandle result = i::Execution::TryCall(CcTest::i_isolate(), v8::Utils::OpenHandle((*loop)), v8::Utils::OpenHandle((*global)), 0, nullptr, i::Execution::MessageHandling::kReport, &exception); CHECK(result.is_null()); // TryCall ignores terminate execution, but rerequests the interrupt. CHECK(!args.GetIsolate()->IsExecutionTerminating()); CHECK(CompileRun("1 + 1;").IsEmpty()); } TEST(TerminationInInnerTryCall) { v8::Isolate* isolate = CcTest::isolate(); v8::HandleScope scope(isolate); v8::Local global_template = CreateGlobalTemplate( CcTest::isolate(), TerminateCurrentThread, DoLoopNoCall); global_template->Set( v8_str("inner_try_call_terminate"), v8::FunctionTemplate::New(isolate, InnerTryCallTerminate)); v8::Local context = v8::Context::New(CcTest::isolate(), nullptr, global_template); v8::Context::Scope context_scope(context); { v8::TryCatch try_catch(isolate); CompileRun("inner_try_call_terminate()"); CHECK(try_catch.HasTerminated()); } v8::Maybe result = CompileRun("2 + 2")->Int32Value( v8::Isolate::GetCurrent()->GetCurrentContext()); CHECK_EQ(4, result.FromJust()); CHECK(!v8::Isolate::GetCurrent()->IsExecutionTerminating()); } TEST(TerminateAndTryCall) { i::FLAG_allow_natives_syntax = true; v8::Isolate* isolate = CcTest::isolate(); v8::HandleScope scope(isolate); v8::Local global = CreateGlobalTemplate( isolate, TerminateCurrentThread, DoLoopCancelTerminate); v8::Local context = v8::Context::New(isolate, nullptr, global); v8::Context::Scope context_scope(context); CHECK(!isolate->IsExecutionTerminating()); v8::TryCatch try_catch(isolate); CHECK(!isolate->IsExecutionTerminating()); // Terminate execution has been triggered inside TryCall, but re-requested // to trigger later. CHECK(CompileRun("terminate(); reference_error();").IsEmpty()); CHECK(try_catch.HasCaught()); CHECK(!isolate->IsExecutionTerminating()); v8::Local value = CcTest::global() ->Get(isolate->GetCurrentContext(), v8_str("terminate")) .ToLocalChecked(); CHECK(value->IsFunction()); // The first stack check after terminate has been re-requested fails. CHECK(CompileRun("1 + 1").IsEmpty()); CHECK(!isolate->IsExecutionTerminating()); // V8 then recovers. v8::Maybe result = CompileRun("2 + 2")->Int32Value( v8::Isolate::GetCurrent()->GetCurrentContext()); CHECK_EQ(4, result.FromJust()); CHECK(!isolate->IsExecutionTerminating()); } class ConsoleImpl : public v8::debug::ConsoleDelegate { private: void Log(const v8::debug::ConsoleCallArguments& args, const v8::debug::ConsoleContext&) override { CompileRun("1 + 1"); } }; TEST(TerminateConsole) { i::FLAG_allow_natives_syntax = true; v8::Isolate* isolate = CcTest::isolate(); ConsoleImpl console; v8::debug::SetConsoleDelegate(isolate, &console); v8::HandleScope scope(isolate); v8::Local global = CreateGlobalTemplate( isolate, TerminateCurrentThread, DoLoopCancelTerminate); v8::Local context = v8::Context::New(isolate, nullptr, global); v8::Context::Scope context_scope(context); CHECK(!isolate->IsExecutionTerminating()); v8::TryCatch try_catch(isolate); CHECK(!isolate->IsExecutionTerminating()); CHECK(CompileRun("terminate(); console.log(); fail();").IsEmpty()); CHECK(try_catch.HasCaught()); CHECK(!isolate->IsExecutionTerminating()); } class TerminatorSleeperThread : public v8::base::Thread { public: explicit TerminatorSleeperThread(v8::Isolate* isolate, int sleep_ms) : Thread(Options("TerminatorSlepperThread")), isolate_(isolate), sleep_ms_(sleep_ms) {} void Run() { v8::base::OS::Sleep(v8::base::TimeDelta::FromMilliseconds(sleep_ms_)); CHECK(!isolate_->IsExecutionTerminating()); isolate_->TerminateExecution(); } private: v8::Isolate* isolate_; int sleep_ms_; }; TEST(TerminateRegExp) { // regexp interpreter does not support preemption. #ifndef V8_INTERPRETED_REGEXP i::FLAG_allow_natives_syntax = true; v8::Isolate* isolate = CcTest::isolate(); ConsoleImpl console; v8::debug::SetConsoleDelegate(isolate, &console); v8::HandleScope scope(isolate); v8::Local global = CreateGlobalTemplate( isolate, TerminateCurrentThread, DoLoopCancelTerminate); v8::Local context = v8::Context::New(isolate, nullptr, global); v8::Context::Scope context_scope(context); CHECK(!isolate->IsExecutionTerminating()); v8::TryCatch try_catch(isolate); CHECK(!isolate->IsExecutionTerminating()); CHECK(!CompileRun("var re = /(x+)+y$/; re.test('x');").IsEmpty()); TerminatorSleeperThread terminator(isolate, 100); terminator.Start(); CHECK(CompileRun("re.test('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'); fail();") .IsEmpty()); CHECK(try_catch.HasCaught()); CHECK(!isolate->IsExecutionTerminating()); #endif // V8_INTERPRETED_REGEXP }