diff --git a/test/inspector/inspector-test.cc b/test/inspector/inspector-test.cc index 6983448d028..96ec3378282 100644 --- a/test/inspector/inspector-test.cc +++ b/test/inspector/inspector-test.cc @@ -565,10 +565,24 @@ class InspectorExtension : public InspectorIsolateData::SetupGlobalTask { inspector->Set(isolate, "runNestedMessageLoop", v8::FunctionTemplate::New( isolate, &InspectorExtension::RunNestedMessageLoop)); + inspector->Set( + isolate, "resetContextGroup", + v8::FunctionTemplate::New(isolate, + &InspectorExtension::ResetContextGroup)); global->Set(isolate, "inspector", inspector); } private: + static void ResetContextGroup( + const v8::FunctionCallbackInfo& info) { + // Test hook simulating what an embedder (e.g. Blink's + // MainThreadDebugger::DidClearContextsForFrame) does on local-root + // navigation: reset the inspector context group for the current context. + v8::Local context = info.GetIsolate()->GetCurrentContext(); + InspectorIsolateData* data = InspectorIsolateData::FromContext(context); + data->ResetContextGroup(data->GetContextGroupId(context)); + } + static void FireContextCreated( const v8::FunctionCallbackInfo& info) { v8::Local context = info.GetIsolate()->GetCurrentContext(); diff --git a/test/inspector/isolate-data.cc b/test/inspector/isolate-data.cc index 0e12240c732..0eef3ea41ee 100644 --- a/test/inspector/isolate-data.cc +++ b/test/inspector/isolate-data.cc @@ -145,7 +145,6 @@ v8::Local InspectorIsolateData::GetDefaultContext( } void InspectorIsolateData::ResetContextGroup(int context_group_id) { - v8::SealHandleScope seal_handle_scope(isolate()); inspector_->resetContextGroup(context_group_id); } diff --git a/test/inspector/runtime/console-storage-uaf-reset-context-group.js b/test/inspector/runtime/console-storage-uaf-reset-context-group.js new file mode 100644 index 00000000000..5407d73b4aa --- /dev/null +++ b/test/inspector/runtime/console-storage-uaf-reset-context-group.js @@ -0,0 +1,54 @@ +// Copyright 2026 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. + +// V8ConsoleMessageStorage::addMessage() runs page JS via wrapArguments() and +// then guards `this`-liveness with hasConsoleMessageStorage(contextGroupId). +// That guard only checks key presence, not object identity. If during the JS +// callout the embedder calls resetContextGroup(G) (frees `this`) and then +// ensureConsoleMessageStorage(G) (re-inserts a fresh storage under the same +// key), addMessage() continues writing into the freed `this`. + +const {session, contextGroup, Protocol} = InspectorTest.start( + 'Tests that V8ConsoleMessageStorage::addMessage does not UAF when the ' + + 'context group is reset and re-populated during wrapArguments.'); + +(async function test() { + await Protocol.Runtime.enable(); + + Protocol.Runtime.onConsoleAPICalled( + msg => InspectorTest.log('consoleAPICalled: ' + + msg.params.args.map(a => a.value).join(' '))); + + // The error has no own "name", so getErrorProperty() falls through to + // object->Get(), which walks the prototype chain and runs the accessor. + // The first getter invocation comes from V8ValueStringBuilder::toString() + // inside createForConsoleAPI() (before addMessage). The second invocation + // comes from descriptionForError() inside addMessage -> wrapArguments, + // which is where the live `this` storage can be freed under the weak guard. + await Protocol.Runtime.evaluate({ + expression: ` + var calls = 0; + Object.defineProperty(Error.prototype, 'name', { + configurable: true, + get() { + if (++calls !== 2) return 'E'; + // (1) Free the V8ConsoleMessageStorage that the outer addMessage() + // is currently running on (simulates embedder local-root reset). + inspector.resetContextGroup(); + // (2) Re-insert a fresh storage under the same contextGroupId so + // hasConsoleMessageStorage() returns true and addMessage() + // proceeds to write into the freed object. + // m_contextIdToGroupIdMap is not erased by resetContextGroup, + // so this resolves to the same group id. + console.log('reinsert'); + return 'E'; + } + }); + console.log(new Error('outer')); + ` + }); + + InspectorTest.log('Did not crash.'); + InspectorTest.completeTest(); +})();