Commit 53fc4807 authored by Benedikt Meurer's avatar Benedikt Meurer Committed by Commit Bot

[debug] Disallow LiveEdit of active frames.

Previously we'd allow to replace the source of functions that are on the
current execution stack under certain conditions, but this has resulted
in an endless stream of bugs due to weird edge cases, and so we're now
limiting LiveEdit to functions that don't have any activation (including
not a suspended generator / async function activation).

We might eventually add the ability to LiveEdit functions with
activations and have them "upgrade upon next invocation", but that
doesn't seem to be an extremely important use case right now.

Fixed: chromium:1195927
Change-Id: I87a45ba4d0ddcfbf867bd4e73738d76b2d789e04
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2846892
Auto-Submit: Benedikt Meurer <bmeurer@chromium.org>
Commit-Queue: Benedikt Meurer <bmeurer@chromium.org>
Reviewed-by: 's avatarYang Guo <yangguo@chromium.org>
Cr-Commit-Position: refs/heads/master@{#74249}
parent 786d4861
......@@ -119,11 +119,7 @@ struct LiveEditResult {
OK,
COMPILE_ERROR,
BLOCKED_BY_RUNNING_GENERATOR,
BLOCKED_BY_FUNCTION_ABOVE_BREAK_FRAME,
BLOCKED_BY_FUNCTION_BELOW_NON_DROPPABLE_FRAME,
BLOCKED_BY_ACTIVE_FUNCTION,
BLOCKED_BY_NEW_TARGET_IN_RESTART_FRAME,
FRAME_RESTART_IS_NOT_SUPPORTED
BLOCKED_BY_ACTIVE_FUNCTION
};
Status status = OK;
bool stack_changed = false;
......
......@@ -782,10 +782,8 @@ bool ParseScript(Isolate* isolate, Handle<Script> script, ParseInfo* parse_info,
}
struct FunctionData {
FunctionData(FunctionLiteral* literal, bool should_restart)
: literal(literal),
stack_position(NOT_ON_STACK),
should_restart(should_restart) {}
explicit FunctionData(FunctionLiteral* literal)
: literal(literal), stack_position(NOT_ON_STACK) {}
FunctionLiteral* literal;
MaybeHandle<SharedFunctionInfo> shared;
......@@ -794,23 +792,14 @@ struct FunctionData {
// In case of multiple functions with different stack position, the latest
// one (in the order below) is used, since it is the most restrictive.
// This is important only for functions to be restarted.
enum StackPosition {
NOT_ON_STACK,
ABOVE_BREAK_FRAME,
PATCHABLE,
BELOW_NON_DROPPABLE_FRAME,
ARCHIVED_THREAD,
};
enum StackPosition { NOT_ON_STACK, ON_STACK };
StackPosition stack_position;
bool should_restart;
};
class FunctionDataMap : public ThreadVisitor {
public:
void AddInterestingLiteral(int script_id, FunctionLiteral* literal,
bool should_restart) {
map_.emplace(GetFuncId(script_id, literal),
FunctionData{literal, should_restart});
void AddInterestingLiteral(int script_id, FunctionLiteral* literal) {
map_.emplace(GetFuncId(script_id, literal), FunctionData{literal});
}
bool Lookup(SharedFunctionInfo sfi, FunctionData** data) {
......@@ -827,7 +816,7 @@ class FunctionDataMap : public ThreadVisitor {
return Lookup(GetFuncId(script->id(), literal), data);
}
void Fill(Isolate* isolate, Address* restart_frame_fp) {
void Fill(Isolate* isolate) {
{
HeapObjectIterator iterator(isolate->heap(),
HeapObjectIterator::kFilterUnreachable);
......@@ -854,38 +843,11 @@ class FunctionDataMap : public ThreadVisitor {
}
}
}
FunctionData::StackPosition stack_position =
isolate->debug()->break_frame_id() == StackFrameId::NO_ID
? FunctionData::PATCHABLE
: FunctionData::ABOVE_BREAK_FRAME;
for (StackFrameIterator it(isolate); !it.done(); it.Advance()) {
StackFrame* frame = it.frame();
if (stack_position == FunctionData::ABOVE_BREAK_FRAME) {
if (frame->id() == isolate->debug()->break_frame_id()) {
stack_position = FunctionData::PATCHABLE;
}
}
if (stack_position == FunctionData::PATCHABLE &&
(frame->is_exit() || frame->is_builtin_exit())) {
stack_position = FunctionData::BELOW_NON_DROPPABLE_FRAME;
continue;
}
if (!frame->is_java_script()) continue;
std::vector<Handle<SharedFunctionInfo>> sfis;
JavaScriptFrame::cast(frame)->GetFunctions(&sfis);
for (auto& sfi : sfis) {
if (stack_position == FunctionData::PATCHABLE &&
IsResumableFunction(sfi->kind())) {
stack_position = FunctionData::BELOW_NON_DROPPABLE_FRAME;
}
FunctionData* data = nullptr;
if (!Lookup(*sfi, &data)) continue;
if (!data->should_restart) continue;
data->stack_position = stack_position;
*restart_frame_fp = frame->fp();
}
}
// Visit the current thread stack.
VisitThread(isolate, isolate->thread_local_top());
// Visit the stacks of all archived threads.
isolate->thread_manager()->IterateArchivedThreads(this);
}
......@@ -932,7 +894,7 @@ class FunctionDataMap : public ThreadVisitor {
for (auto& sfi : sfis) {
FunctionData* data = nullptr;
if (!Lookup(*sfi, &data)) continue;
data->stack_position = FunctionData::ARCHIVED_THREAD;
data->stack_position = FunctionData::ON_STACK;
}
}
}
......@@ -944,7 +906,6 @@ bool CanPatchScript(
const LiteralMap& changed, Handle<Script> script, Handle<Script> new_script,
FunctionDataMap& function_data_map, // NOLINT(runtime/references)
debug::LiveEditResult* result) {
debug::LiveEditResult::Status status = debug::LiveEditResult::OK;
for (const auto& mapping : changed) {
FunctionData* data = nullptr;
function_data_map.Lookup(script, mapping.first, &data);
......@@ -953,55 +914,11 @@ bool CanPatchScript(
Handle<SharedFunctionInfo> sfi;
if (!data->shared.ToHandle(&sfi)) {
continue;
} else if (!data->should_restart) {
UNREACHABLE();
} else if (data->stack_position == FunctionData::ABOVE_BREAK_FRAME) {
status = debug::LiveEditResult::BLOCKED_BY_FUNCTION_ABOVE_BREAK_FRAME;
} else if (data->stack_position ==
FunctionData::BELOW_NON_DROPPABLE_FRAME) {
status =
debug::LiveEditResult::BLOCKED_BY_FUNCTION_BELOW_NON_DROPPABLE_FRAME;
} else if (!data->running_generators.empty()) {
status = debug::LiveEditResult::BLOCKED_BY_RUNNING_GENERATOR;
} else if (data->stack_position == FunctionData::ARCHIVED_THREAD) {
status = debug::LiveEditResult::BLOCKED_BY_ACTIVE_FUNCTION;
}
if (status != debug::LiveEditResult::OK) {
result->status = status;
} else if (data->stack_position == FunctionData::ON_STACK) {
result->status = debug::LiveEditResult::BLOCKED_BY_ACTIVE_FUNCTION;
return false;
}
}
return true;
}
bool CanRestartFrame(
Isolate* isolate, Address fp,
FunctionDataMap& function_data_map, // NOLINT(runtime/references)
const LiteralMap& changed, debug::LiveEditResult* result) {
DCHECK_GT(fp, 0);
StackFrame* restart_frame = nullptr;
StackFrameIterator it(isolate);
for (; !it.done(); it.Advance()) {
if (it.frame()->fp() == fp) {
restart_frame = it.frame();
break;
}
}
DCHECK(restart_frame && restart_frame->is_java_script());
if (!LiveEdit::kFrameDropperSupported) {
result->status = debug::LiveEditResult::FRAME_RESTART_IS_NOT_SUPPORTED;
return false;
}
std::vector<Handle<SharedFunctionInfo>> sfis;
JavaScriptFrame::cast(restart_frame)->GetFunctions(&sfis);
for (auto& sfi : sfis) {
FunctionData* data = nullptr;
if (!function_data_map.Lookup(*sfi, &data)) continue;
auto new_literal_it = changed.find(data->literal);
if (new_literal_it == changed.end()) continue;
if (new_literal_it->second->scope()->new_target_var()) {
result->status =
debug::LiveEditResult::BLOCKED_BY_NEW_TARGET_IN_RESTART_FRAME;
} else if (!data->running_generators.empty()) {
result->status = debug::LiveEditResult::BLOCKED_BY_RUNNING_GENERATOR;
return false;
}
}
......@@ -1092,24 +1009,17 @@ void LiveEdit::PatchScript(Isolate* isolate, Handle<Script> script,
FunctionDataMap function_data_map;
for (const auto& mapping : changed) {
function_data_map.AddInterestingLiteral(script->id(), mapping.first, true);
function_data_map.AddInterestingLiteral(new_script->id(), mapping.second,
false);
function_data_map.AddInterestingLiteral(script->id(), mapping.first);
function_data_map.AddInterestingLiteral(new_script->id(), mapping.second);
}
for (const auto& mapping : unchanged) {
function_data_map.AddInterestingLiteral(script->id(), mapping.first, false);
function_data_map.AddInterestingLiteral(script->id(), mapping.first);
}
Address restart_frame_fp = 0;
function_data_map.Fill(isolate, &restart_frame_fp);
function_data_map.Fill(isolate);
if (!CanPatchScript(changed, script, new_script, function_data_map, result)) {
return;
}
if (restart_frame_fp &&
!CanRestartFrame(isolate, restart_frame_fp, function_data_map, changed,
result)) {
return;
}
if (preview) {
result->status = debug::LiveEditResult::OK;
......@@ -1273,16 +1183,6 @@ void LiveEdit::PatchScript(Isolate* isolate, Handle<Script> script,
}
#endif
if (restart_frame_fp) {
for (StackFrameIterator it(isolate); !it.done(); it.Advance()) {
if (it.frame()->fp() == restart_frame_fp) {
isolate->debug()->ScheduleFrameRestart(it.frame());
result->stack_changed = true;
break;
}
}
}
int script_id = script->id();
script->set_id(new_script->id());
new_script->set_id(script_id);
......
......@@ -868,22 +868,9 @@ RUNTIME_FUNCTION(Runtime_LiveEditPatchScript) {
case v8::debug::LiveEditResult::BLOCKED_BY_RUNNING_GENERATOR:
return isolate->Throw(*isolate->factory()->NewStringFromAsciiChecked(
"LiveEdit failed: BLOCKED_BY_RUNNING_GENERATOR"));
case v8::debug::LiveEditResult::BLOCKED_BY_FUNCTION_ABOVE_BREAK_FRAME:
return isolate->Throw(*isolate->factory()->NewStringFromAsciiChecked(
"LiveEdit failed: BLOCKED_BY_FUNCTION_ABOVE_BREAK_FRAME"));
case v8::debug::LiveEditResult::
BLOCKED_BY_FUNCTION_BELOW_NON_DROPPABLE_FRAME:
return isolate->Throw(*isolate->factory()->NewStringFromAsciiChecked(
"LiveEdit failed: BLOCKED_BY_FUNCTION_BELOW_NON_DROPPABLE_FRAME"));
case v8::debug::LiveEditResult::BLOCKED_BY_ACTIVE_FUNCTION:
return isolate->Throw(*isolate->factory()->NewStringFromAsciiChecked(
"LiveEdit failed: BLOCKED_BY_ACTIVE_FUNCTION"));
case v8::debug::LiveEditResult::BLOCKED_BY_NEW_TARGET_IN_RESTART_FRAME:
return isolate->Throw(*isolate->factory()->NewStringFromAsciiChecked(
"LiveEdit failed: BLOCKED_BY_NEW_TARGET_IN_RESTART_FRAME"));
case v8::debug::LiveEditResult::FRAME_RESTART_IS_NOT_SUPPORTED:
return isolate->Throw(*isolate->factory()->NewStringFromAsciiChecked(
"LiveEdit failed: FRAME_RESTART_IS_NOT_SUPPORTED"));
case v8::debug::LiveEditResult::OK:
return ReadOnlyRoots(isolate).undefined_value();
}
......
......@@ -136,13 +136,6 @@ test = new TestBase("Test without function on stack");
test.ScriptChanger();
assertEquals("Capybara", test.ChooseAnimal(Noop));
test = new TestBase("Test with function on stack");
assertEquals("Capybara", test.ChooseAnimal(WrapInDebuggerCall(WrapInRestartProof(test.ScriptChanger))));
test = new TestBase("Test with function on stack and with constructor frame");
assertEquals("Capybara", test.ChooseAnimal(WrapInConstructor(WrapInDebuggerCall(WrapInRestartProof(test.ScriptChanger)))));
test = new TestBase("Test with C++ frame above ChooseAnimal frame");
exception_holder = {};
assertEquals("Cat", test.ChooseAnimal(WrapInNativeCall(WrapInDebuggerCall(WrapInCatcher(test.ScriptChanger, exception_holder)))));
......
// Copyright 2012 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.
// Flags: --noalways-opt
Debug = debug.Debug
function TestCase(test_scenario, expected_output) {
// Global variable, accessed from eval'd script.
test_output = "";
var script_text_generator = (function() {
var variables = { a: 1, b: 1, c: 1, d: 1, e: 1, f: 1 };
return {
get: function() {
return "(function() {\n " +
" function A() {\n " +
" test_output += 'a' + " + variables.a + ";\n " +
" test_output += '=';\n " +
" debugger;\n " +
" return 'Capybara';\n " +
" }\n " +
" function B(p1, p2) {\n " +
" test_output += 'b' + " + variables.b + ";\n " +
" return A();\n " +
" }\n " +
" function C() {\n " +
" test_output += 'c' + " + variables.c + ";\n " +
" // Function call with argument adaptor is intentional.\n " +
" return B();\n " +
" }\n " +
" function D() {\n " +
" test_output += 'd' + " + variables.d + ";\n " +
" // Function call with argument adaptor is intentional.\n " +
" return C(1, 2);\n " +
" }\n " +
" function E() {\n " +
" test_output += 'e' + " + variables.e + ";\n " +
" return D();\n " +
" }\n " +
" function F() {\n " +
" test_output += 'f' + " + variables.f + ";\n " +
" return E();\n " +
" }\n " +
" return F();\n " +
"})\n";
},
change: function(var_name) {
variables[var_name]++;
}
};
})();
var test_fun = eval(script_text_generator.get());
var scenario_pos = 0;
function DebuggerStatementHandler() {
while (true) {
assertTrue(scenario_pos < test_scenario.length);
var change_var = test_scenario[scenario_pos++];
if (change_var == '=') {
// Continue.
return;
}
script_text_generator.change(change_var);
try {
%LiveEditPatchScript(test_fun, script_text_generator.get())
} catch (e) {
print("LiveEdit exception: " + e);
throw e;
}
}
}
var saved_exception = null;
function listener(event, exec_state, event_data, data) {
if (event == Debug.DebugEvent.Break) {
try {
DebuggerStatementHandler();
} catch (e) {
saved_exception = e;
}
} else {
print("Other: " + event);
}
}
Debug.setListener(listener);
assertEquals("Capybara", test_fun());
Debug.setListener(null);
if (saved_exception) {
print("Exception: " + saved_exception);
assertUnreachable();
}
print(test_output);
assertEquals(expected_output, test_output);
}
TestCase(['='], "f1e1d1c1b1a1=");
TestCase(['c', '=', '='], "f1e1d1c1b1a1=c2b1a1=");
TestCase(['b', 'c', 'd', 'e', '=', '='], "f1e1d1c1b1a1=e2d2c2b2a1=");
TestCase(['b', 'c', '=', 'b', 'c', 'd', 'e', '=', '='], "f1e1d1c1b1a1=c2b2a1=e2d2c3b3a1=");
TestCase(['e', 'f', '=', '='], "f1e1d1c1b1a1=f2e2d1c1b1a1=");
// Copyright 2016 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax
Debug = debug.Debug
function BestEditor() {
throw 'Emacs';
}
var exception = null;
var results = [];
var log = []
function listener(event, exec_state, event_data, data) {
if (event != Debug.DebugEvent.Exception) return;
try {
var source_line = event_data.sourceLineText();
print(source_line);
log.push(source_line);
switch (results.length) {
case 0:
Replace(BestEditor, "Emacs", "Eclipse");
break;
case 1:
Replace(BestEditor, "Eclipse", "Vim");
break;
case 2:
break;
default:
assertUnreachable();
}
} catch (e) {
exception = e;
}
};
function Replace(fun, original, patch) {
if (fun.toString().indexOf(original) < 0) return;
%LiveEditPatchScript(fun, Debug.scriptSource(fun).replace(original, patch));
}
Debug.setListener(listener);
Debug.setBreakOnException();
for (var i = 0; i < 3; i++) {
try {
BestEditor();
} catch (e) {
results.push(e);
}
}
Debug.setListener(null);
assertNull(exception);
assertEquals(["Emacs", "Eclipse", "Vim"], results);
print(JSON.stringify(log, 1));
assertEquals([
" throw 'Emacs';",
" throw 'Eclipse';",
" throw 'Vim';",
], log);
......@@ -21,11 +21,13 @@ let patch = null, exception = null;
Debug.setListener(listener);
patch = ['return 5', 'return 3'];
assertEquals(3, test(2)); // no running generator
assertEquals(5, test(2)); // generator on stack
assertEquals(exception,
'LiveEdit failed: BLOCKED_BY_ACTIVE_FUNCTION');
patch = ['return 3', 'return -1'];
assertEquals(3, test(3)); // there is running generator
assertEquals(5, test(5)); // there is running generator
assertEquals(exception,
'LiveEdit failed: BLOCKED_BY_FUNCTION_BELOW_NON_DROPPABLE_FRAME');
'LiveEdit failed: BLOCKED_BY_ACTIVE_FUNCTION');
Debug.setListener(null);
function listener(event) {
......
// Copyright 2017 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax
Debug = debug.Debug
var counter = 0;
var exception = null;
function f() {
if (++counter > 5) return;
debugger;
return counter;
};
function listener(event, exec_state, event_data, data) {
if (event != Debug.DebugEvent.Break) return;
try {
var original = 'debugger;';
var patch = 'debugger;\n';
%LiveEditPatchScript(f, Debug.scriptSource(f).replace(original, patch));
} catch (e) {
exception = e;
}
}
Debug.setListener(listener);
f();
Debug.setListener(null);
assertNull(exception);
assertEquals(6, counter);
// Copyright 2015 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax
Debug = debug.Debug
function BestEditor() {
return 'Emacs';
}
var exception = null;
var results = [];
var log = []
function listener(event, exec_state, event_data, data) {
if (event != Debug.DebugEvent.Break) return;
try {
var source_line = event_data.sourceLineText();
log.push(source_line);
if (source_line.indexOf("return") >= 0) {
switch (results.length) {
case 0:
break;
case 1:
Replace(BestEditor, "Emacs", "Eclipse");
break;
case 2:
Replace(BestEditor, "Eclipse", "Vim");
break;
default:
assertUnreachable();
}
}
exec_state.prepareStep(Debug.StepAction.StepIn);
} catch (e) {
exception = e;
}
};
function Replace(fun, original, patch) {
if (fun.toString().indexOf(original) < 0) return;
%LiveEditPatchScript(fun, Debug.scriptSource(fun).replace(original, patch));
}
Debug.setListener(listener);
debugger;
results.push(BestEditor());
results.push(BestEditor());
results.push(BestEditor());
Debug.setListener(null);
assertNull(exception);
assertEquals(["Emacs", "Eclipse", "Vim"], results);
assertEquals([
"debugger;",
"results.push(BestEditor());",
" return 'Emacs';",
" return 'Emacs';",
"results.push(BestEditor());",
"results.push(BestEditor());",
" return 'Emacs';",
" return 'Eclipse';",
" return 'Eclipse';",
"results.push(BestEditor());",
"results.push(BestEditor());",
" return 'Eclipse';",
" return 'Vim';",
" return 'Vim';",
"results.push(BestEditor());",
"Debug.setListener(null);"
], log);
......@@ -46,7 +46,7 @@ function Replace(fun, original, patch) {
try {
%LiveEditPatchScript(fun, Debug.scriptSource(fun).replace(original, patch));
} catch (e) {
assertEquals(e, 'LiveEdit failed: BLOCKED_BY_NEW_TARGET_IN_RESTART_FRAME');
assertEquals(e, 'LiveEdit failed: BLOCKED_BY_ACTIVE_FUNCTION');
exceptions++;
}
});
......
......@@ -43,7 +43,7 @@ function Replace(fun, original, patch) {
try {
%LiveEditPatchScript(fun, Debug.scriptSource(fun).replace(original, patch));
} catch (e) {
assertEquals(e, 'LiveEdit failed: BLOCKED_BY_NEW_TARGET_IN_RESTART_FRAME');
assertEquals(e, 'LiveEdit failed: BLOCKED_BY_ACTIVE_FUNCTION');
exceptions++;
}
});
......
......@@ -83,6 +83,6 @@ assertEquals([LogNewTarget, LogNewTarget], results);
replace_again = true;
Wrapper();
assertEquals(3, construct_calls);
assertEquals(4, wrapper_calls); // Restarts
assertEquals(0, exceptions); // Replace succeeds
assertEquals(3, wrapper_calls);
assertEquals(1, exceptions); // Replace failed
assertEquals([LogNewTarget, LogNewTarget, LogNewTarget], results);
// Copyright 2014 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax
var Debug = debug.Debug;
unique_id = 0;
var Generator = (function*(){}).constructor;
function assertIteratorResult(value, done, result) {
assertEquals({value: value, done: done}, result);
}
function MakeGenerator() {
// Prevents eval script caching.
unique_id++;
return Generator('callback',
"/* " + unique_id + "*/\n" +
"yield callback();\n" +
"return 'Cat';\n");
}
function MakeFunction() {
// Prevents eval script caching.
unique_id++;
return Function('callback',
"/* " + unique_id + "*/\n" +
"callback();\n" +
"return 'Cat';\n");
}
// First, try MakeGenerator with no perturbations.
(function(){
var generator = MakeGenerator();
function callback() {};
var iter = generator(callback);
assertIteratorResult(undefined, false, iter.next());
assertIteratorResult("Cat", true, iter.next());
})();
function ExecuteInDebugContext(f) {
var result;
var exception = null;
Debug.setListener(function(event) {
if (event == Debug.DebugEvent.Break) {
try {
result = f();
} catch (e) {
// Rethrow this exception later.
exception = e;
}
}
});
debugger;
Debug.setListener(null);
if (exception !== null) throw exception;
return result;
}
function patch(fun, from, to) {
function debug() {
%LiveEditPatchScript(fun, Debug.scriptSource(fun).replace(from, to));
}
ExecuteInDebugContext(debug);
}
// Try to edit a MakeGenerator while it's running, then again while it's
// stopped.
(function(){
var generator = MakeGenerator();
var gen_patch_attempted = false;
function attempt_gen_patch() {
assertFalse(gen_patch_attempted);
gen_patch_attempted = true;
assertThrowsEquals(function() {
patch(generator, '\'Cat\'', '\'Capybara\'')
}, 'LiveEdit failed: BLOCKED_BY_FUNCTION_BELOW_NON_DROPPABLE_FRAME');
};
var iter = generator(attempt_gen_patch);
assertIteratorResult(undefined, false, iter.next());
// Patch should not succeed because there is a live generator activation on
// the stack.
assertIteratorResult("Cat", true, iter.next());
assertTrue(gen_patch_attempted);
// At this point one iterator is live, but closed, so the patch will succeed.
patch(generator, "'Cat'", "'Capybara'");
iter = generator(function(){});
assertIteratorResult(undefined, false, iter.next());
// Patch successful.
assertIteratorResult("Capybara", true, iter.next());
// Patching will fail however when a live iterator is suspended.
iter = generator(function(){});
assertIteratorResult(undefined, false, iter.next());
assertThrowsEquals(function() {
patch(generator, '\'Capybara\'', '\'Tapir\'')
}, 'LiveEdit failed: BLOCKED_BY_RUNNING_GENERATOR');
assertIteratorResult("Capybara", true, iter.next());
// Try to patch functions with activations inside and outside generator
// function activations. We should succeed in the former case, but not in the
// latter.
var fun_outside = MakeFunction();
var fun_inside = MakeFunction();
var fun_patch_attempted = false;
var fun_patch_restarted = false;
function attempt_fun_patches() {
if (fun_patch_attempted) {
assertFalse(fun_patch_restarted);
fun_patch_restarted = true;
return;
}
fun_patch_attempted = true;
// Patching outside a generator activation must fail.
assertThrowsEquals(function() {
patch(fun_outside, '\'Cat\'', '\'Cobra\'')
}, 'LiveEdit failed: BLOCKED_BY_FUNCTION_BELOW_NON_DROPPABLE_FRAME');
// Patching inside a generator activation may succeed.
patch(fun_inside, "'Cat'", "'Koala'");
}
iter = generator(function() { return fun_inside(attempt_fun_patches) });
assertEquals('Cat',
fun_outside(function () {
assertIteratorResult('Koala', false, iter.next());
assertTrue(fun_patch_restarted);
}));
})();
// Copyright 2016 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax
var Debug = debug.Debug;
unique_id = 0;
var AsyncFunction = (async function(){}).constructor;
function assertPromiseValue(value, promise) {
promise.then(resolve => {
went = true;
if (resolve !== value) {
print(`expected ${value} found ${resolve}`);
quit(1);
}
}, reject => {
print(`rejected ${reject}`);
quit(1);
});
}
function MakeAsyncFunction() {
// Prevents eval script caching.
unique_id++;
return AsyncFunction('callback',
"/* " + unique_id + "*/\n" +
"await callback();\n" +
"return 'Cat';\n");
}
function MakeFunction() {
// Prevents eval script caching.
unique_id++;
return Function('callback',
"/* " + unique_id + "*/\n" +
"callback();\n" +
"return 'Cat';\n");
}
// First, try MakeGenerator with no perturbations.
(function(){
var asyncfn = MakeAsyncFunction();
function callback() {};
var promise = asyncfn(callback);
assertPromiseValue('Cat', promise);
})();
function ExecuteInDebugContext(f) {
var result;
var exception = null;
Debug.setListener(function(event) {
if (event == Debug.DebugEvent.Break) {
try {
result = f();
} catch (e) {
// Rethrow this exception later.
exception = e;
}
}
});
debugger;
Debug.setListener(null);
if (exception !== null) throw exception;
return result;
}
function patch(fun, from, to) {
function debug() {
%LiveEditPatchScript(fun, Debug.scriptSource(fun).replace(from, to));
}
ExecuteInDebugContext(debug);
}
// Try to edit a MakeAsyncFunction while it's running, then again while it's
// stopped.
(function(){
var asyncfn = MakeAsyncFunction();
var patch_attempted = false;
function attempt_patch() {
assertFalse(patch_attempted);
patch_attempted = true;
assertThrowsEquals(function() {
patch(asyncfn, '\'Cat\'', '\'Capybara\'')
}, 'LiveEdit failed: BLOCKED_BY_FUNCTION_BELOW_NON_DROPPABLE_FRAME');
};
var promise = asyncfn(attempt_patch);
// Patch should not succeed because there is a live async function activation
// on the stack.
assertPromiseValue("Cat", promise);
assertTrue(patch_attempted);
%PerformMicrotaskCheckpoint();
// At this point one iterator is live, but closed, so the patch will succeed.
patch(asyncfn, "'Cat'", "'Capybara'");
promise = asyncfn(function(){});
// Patch successful.
assertPromiseValue("Capybara", promise);
// Patching will fail however when an async function is suspended.
var resolve;
promise = asyncfn(function(){return new Promise(function(r){resolve = r})});
assertThrowsEquals(function() {
patch(asyncfn, '\'Capybara\'', '\'Tapir\'')
}, 'LiveEdit failed: BLOCKED_BY_RUNNING_GENERATOR');
resolve();
assertPromiseValue("Capybara", promise);
// Try to patch functions with activations inside and outside async
// function activations. We should succeed in the former case, but not in the
// latter.
var fun_outside = eval('((callback) => { callback(); return \'Cat\';})');
var fun_inside = MakeFunction();
var fun_patch_attempted = false;
var fun_patch_restarted = false;
function attempt_fun_patches() {
if (fun_patch_attempted) {
assertFalse(fun_patch_restarted);
fun_patch_restarted = true;
return;
}
fun_patch_attempted = true;
// Patching outside an async function activation must fail.
assertThrowsEquals(function() {
patch(fun_outside, '\'Cat\'', '\'Cobra\'')
}, 'LiveEdit failed: BLOCKED_BY_FUNCTION_BELOW_NON_DROPPABLE_FRAME');
// Patching inside an async function activation may succeed.
patch(fun_inside, "'Cat'", "'Koala'");
}
result = fun_outside(() => asyncfn(function() {
return fun_inside(attempt_fun_patches);
}));
assertEquals('Cat',
fun_outside(function () {
assertEquals(result, 'Cat');
assertTrue(fun_patch_restarted);
assertTrue(fun_inside.toString().includes("'Koala'"));
}));
})();
%PerformMicrotaskCheckpoint();
......@@ -70,7 +70,6 @@
# https://crbug.com/v8/8141
'debug/debug-liveedit-1': [SKIP],
'debug/debug-liveedit-double-call': [SKIP],
'debug/es6/debug-liveedit-new-target-3': [SKIP],
'debug/side-effect/debug-evaluate-no-side-effect-control': [SKIP],
}], # 'gc_stress == True'
......@@ -126,7 +125,6 @@
# Stack manipulations in LiveEdit is not implemented for this arch.
'debug/debug-liveedit-check-stack': [SKIP],
'debug/debug-liveedit-double-call': [SKIP],
'debug/debug-liveedit-restart-frame': [SKIP],
}], # 'arch == s390 or arch == s390x'
......
Tests Debugger.setScriptSource
Running test: addLineAfter
var x = a;
#debugger;
return x + b;
---
Break location after LiveEdit:
var x = a;
#debugger;
var x = 3;
stackChanged: true
Protocol.Debugger.stepInto
function foo(a,b,c) {
var x = #a;
debugger;
Running test: addLineBefore
var x = a;
#debugger;
return x + b;
---
Break location after LiveEdit:
var x = a;
#var x = 3;
debugger;
stackChanged: true
Protocol.Debugger.stepInto
function foo(a,b,c) {
var x = #a;
var x = 3;
Running test: breakAtFirstLineAddLineAfter
function boo() {
#debugger;
var x = 1;
---
Break location after LiveEdit:
function boo() {
#debugger;
var x = 3;
stackChanged: true
Protocol.Debugger.stepInto
function boo() {
#debugger;
var x = 3;
Running test: breakAtFirstLineAddLineBefore
function boo() {
#debugger;
var x = 1;
---
Break location after LiveEdit:
function boo() {
#var x = 3;
debugger;
stackChanged: true
Protocol.Debugger.stepInto
function boo() {
var x = #3;
debugger;
// Copyright 2017 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --no-always-opt
let {session, contextGroup, Protocol} =
InspectorTest.start('Tests Debugger.setScriptSource');
session.setupScriptMap();
function foo(a,b,c) {
var x = a;
debugger;
return x + b;
}
function boo() {
debugger;
var x = 1;
return x + 2;
}
InspectorTest.runAsyncTestSuite([
async function addLineAfter() {
await Protocol.Debugger.enable();
Protocol.Runtime.evaluate({expression: foo.toString()});
let {params:{scriptId}} = await Protocol.Debugger.onceScriptParsed();
Protocol.Runtime.evaluate({
expression: 'setTimeout(() => foo(1,2,3), 0)//# sourceURL=test.js'});
let {params:{callFrames}} = await Protocol.Debugger.oncePaused();
await session.logSourceLocation(callFrames[0].location);
await replaceInSource(scriptId, 'debugger;', 'debugger;\nvar x = 3;');
Protocol.Debugger.resume();
await Protocol.Debugger.oncePaused();
await Protocol.Debugger.disable();
},
async function addLineBefore() {
await Protocol.Debugger.enable();
Protocol.Runtime.evaluate({expression: foo.toString()});
let {params:{scriptId}} = await Protocol.Debugger.onceScriptParsed();
Protocol.Runtime.evaluate({
expression: 'setTimeout(foo, 0)//# sourceURL=test.js'});
let {params:{callFrames}} = await Protocol.Debugger.oncePaused();
await session.logSourceLocation(callFrames[0].location);
await replaceInSource(scriptId, 'debugger;', 'var x = 3;\ndebugger;');
Protocol.Debugger.resume();
await Protocol.Debugger.oncePaused();
await Protocol.Debugger.disable();
},
async function breakAtFirstLineAddLineAfter() {
await Protocol.Debugger.enable();
Protocol.Runtime.evaluate({expression: boo.toString()});
let {params:{scriptId}} = await Protocol.Debugger.onceScriptParsed();
Protocol.Runtime.evaluate({
expression: 'setTimeout(boo, 0)//# sourceURL=test.js'});
let {params:{callFrames}} = await Protocol.Debugger.oncePaused();
await session.logSourceLocation(callFrames[0].location);
await replaceInSource(scriptId, 'debugger;', 'debugger;\nvar x = 3;');
await Protocol.Debugger.disable();
},
async function breakAtFirstLineAddLineBefore() {
await Protocol.Debugger.enable();
Protocol.Runtime.evaluate({expression: boo.toString()});
let {params:{scriptId}} = await Protocol.Debugger.onceScriptParsed();
Protocol.Runtime.evaluate({
expression: 'setTimeout(boo, 0)//# sourceURL=test.js'});
let {params:{callFrames}} = await Protocol.Debugger.oncePaused();
await session.logSourceLocation(callFrames[0].location);
await replaceInSource(scriptId, 'debugger;', 'var x = 3;\ndebugger;');
await Protocol.Debugger.disable();
}
]);
async function replaceInSource(scriptId, oldString, newString) {
InspectorTest.log('---');
let {result:{scriptSource}} =
await Protocol.Debugger.getScriptSource({scriptId});
let {result} = await Protocol.Debugger.setScriptSource({
scriptId,
scriptSource: scriptSource.replace(oldString, newString)
});
InspectorTest.log('Break location after LiveEdit:');
await session.logSourceLocation(result.callFrames[0].location, true);
InspectorTest.log('stackChanged: ' + result.stackChanged);
if (result.stackChanged) {
InspectorTest.log('Protocol.Debugger.stepInto');
Protocol.Debugger.stepInto();
var {params:{callFrames}} = await Protocol.Debugger.oncePaused();
await session.logSourceLocation(callFrames[0].location);
}
}
Change return string constant at breakpoint
Set breakpoint inside f() and call function..
Paused at:
(function f() {
#return 'Cat';
})
Change Cat to Capybara..
Paused at:
(function f() {
#return 'Capybara';
})
Resume and check return value..
SlimFunction() = Capybara
// 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.
let {session, contextGroup, Protocol} =
InspectorTest.start('Change return string constant at breakpoint');
contextGroup.addScript(
`SlimFunction = eval(
'(function f() {\\n ' +
' return \\'Cat\\';\\n' +
'})\\n' +
'//# sourceURL=eval.js\\n');`);
(async function test() {
session.setupScriptMap();
let scriptPromise = new Promise(resolve => {
Protocol.Debugger.onScriptParsed(({params}) => {
if (params.url === 'eval.js') {
resolve(params);
Protocol.Debugger.onScriptParsed(null);
}
});
});
Protocol.Debugger.enable();
const script = await scriptPromise;
InspectorTest.log('Set breakpoint inside f() and call function..');
const {result:{actualLocation}} = await Protocol.Debugger.setBreakpoint({
location: { scriptId: script.scriptId, lineNumber: 1, columnNumber: 0}});
const evalPromise = Protocol.Runtime.evaluate({expression: 'SlimFunction()'});
const {params:{callFrames}} = await Protocol.Debugger.oncePaused();
InspectorTest.log('Paused at:');
await session.logSourceLocation(callFrames[0].location);
InspectorTest.log('Change Cat to Capybara..');
const {result:{scriptSource}} = await Protocol.Debugger.getScriptSource({
scriptId: script.scriptId
});
const {result:{callFrames:[topFrame],stackChanged}} =
await Protocol.Debugger.setScriptSource({
scriptId: script.scriptId,
scriptSource: scriptSource.replace(`'Cat'`, `'Capybara'`)
});
InspectorTest.log('Paused at:');
await session.logSourceLocation(topFrame.location, true);
InspectorTest.log('Resume and check return value..');
Protocol.Debugger.resume();
InspectorTest.log(
`SlimFunction() = ${(await evalPromise).result.result.value}`);
InspectorTest.completeTest();
})();
......@@ -108,8 +108,6 @@
##############################################################################
['arch == s390 or arch == s390x', {
# Stack manipulations in LiveEdit is not implemented for this arch.
'debugger/set-script-source-stack-padding': [SKIP],
# Liftoff needs to be enabled before running these tests.
'debugger/wasm-*': [SKIP],
}], # 'arch == s390 or arch == s390x'
......@@ -345,9 +343,7 @@
'debugger/set-breakpoint-url-regex': [SKIP],
'debugger/set-instrumentation-breakpoint': [SKIP],
'debugger/set-script-source': [SKIP],
'debugger/set-script-source-2': [SKIP],
'debugger/set-script-source-exception': [SKIP],
'debugger/set-script-source-stack-padding': [SKIP],
'debugger/set-script-source-unchanged': [SKIP],
'debugger/set-skip-all-pauses': [SKIP],
'debugger/set-variable-value': [SKIP],
......
Regression test for crbug.com/1195927
Running test: test
Debugger.setBreakpoint result:
{
actualLocation : {
columnNumber : 2
lineNumber : 2
scriptId : <scriptId>
}
breakpointId : <breakpointId>
}
Debugger.paused call frames:
foo (foo.js:2:2)
(anonymous) (:0:0)
Debugger.setScriptSource result:
{
exceptionDetails : {
columnNumber : 0
exceptionId : <exceptionId>
lineNumber : 0
text :
}
}
foo(42) result:
{
result : {
description : 43
type : number
value : 43
}
}
// Copyright 2021 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const {session, contextGroup, Protocol} =
InspectorTest.start('Regression test for crbug.com/1195927');
const source = `
function foo(x) {
x = x + 1;
return x;
}
`;
const newSource = `
function foo(x) {
x = x + 2;
return x;
}
`;
InspectorTest.runAsyncTestSuite([
async function test() {
session.setupScriptMap();
await Protocol.Runtime.enable();
await Protocol.Debugger.enable();
contextGroup.addScript(source, 0, 0, 'foo.js');
const { params: { scriptId } } = await Protocol.Debugger.onceScriptParsed();
let {result} = await Protocol.Debugger.setBreakpoint({location: {
scriptId,
lineNumber: 2,
}});
InspectorTest.log('Debugger.setBreakpoint result:');
InspectorTest.logMessage(result);
const callPromise = Protocol.Runtime.evaluate({expression: 'foo(42)'});
let {params: {callFrames}} = await Protocol.Debugger.oncePaused();
InspectorTest.log('Debugger.paused call frames:');
session.logCallFrames(callFrames);
({result} = await Protocol.Debugger.setScriptSource({
scriptId,
scriptSource: newSource
}));
InspectorTest.log('Debugger.setScriptSource result:');
InspectorTest.logMessage(result);
([, {result}] = await Promise.all([Protocol.Debugger.resume(), callPromise]));
InspectorTest.log('foo(42) result:');
InspectorTest.logMessage(result);
}
]);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment