diff --git a/services/proxy_resolver/proxy_resolver_v8_tracing_unittest.cc b/services/proxy_resolver/proxy_resolver_v8_tracing_unittest.cc index 78337472fa84b..a93442efa0650 100644 --- a/services/proxy_resolver/proxy_resolver_v8_tracing_unittest.cc +++ b/services/proxy_resolver/proxy_resolver_v8_tracing_unittest.cc @@ -1272,6 +1272,93 @@ TEST_F(ProxyResolverV8TracingTest, FinalizationRegistryCleanup) { EXPECT_THAT(callback_b.WaitForResult(), IsOk()); } +// Verifies that Atomics.waitAsync resolution tasks posted from one resolver +// do not re-enter the C++ Context of another resolver after that Context has +// been freed. ~Context()'s ContextDisposedNotification prunes dirty +// FinalizationRegistries, but does NOT clear the FutexWaitList, cancel queued +// ResolveAsyncWaiterPromisesTask foreground tasks, or flush the isolate's +// shared default MicrotaskQueue. +// +// NOTE: This test must be run in isolation +// (--gtest_filter=ProxyResolverV8TracingTest.AtomicsWaitAsyncAfterContextDisposed) +// so that resolver B's worker thread is the first to construct the shared +// gin::IsolateHolder and therefore becomes the isolate's foreground task +// runner. +TEST_F(ProxyResolverV8TracingTest, AtomicsWaitAsyncAfterContextDisposed) { + DeferredProxyHostResolver host_resolver; + MockBindings mock_bindings_b(&host_resolver); + MockBindings mock_bindings_a(&host_resolver); + + base::RunLoop run_loop_second; + host_resolver.SetOnSecondRequest(run_loop_second.QuitClosure()); + + // Step 1: Create resolver B. Its worker thread becomes the shared isolate's + // foreground task runner. + scoped_refptr script_b = net::PacFileData::FromUTF8( + "function FindProxyForURL(url, host) {\n" + " dnsResolve('first');\n" + " dnsResolve('second');\n" + " return 'DIRECT';\n" + "}\n"); + std::unique_ptr resolver_b = + CreateResolverWithScriptData(mock_bindings_b.CreateBindings(), script_b); + + // Step 2: Start a request on B. Non-blocking DNS tracing terminates and + // restarts execution as it discovers each DNS dependency; after 'second' is + // requested, DeferredProxyHostResolver leaves it pending and B's worker + // thread returns to its message loop, idle and not holding the v8::Locker. + net::TestCompletionCallback callback_b; + net::ProxyInfo proxy_info_b; + std::unique_ptr req_b; + resolver_b->GetProxyForURL( + GURL("http://foo/"), net::NetworkAnonymizationKey(), &proxy_info_b, + callback_b.callback(), &req_b, mock_bindings_b.CreateBindings()); + + run_loop_second.Run(); + + // Step 3: Create resolver A. Its top-level script arms an Atomics.waitAsync + // waiter and immediately notifies it. Atomics.notify -> + // FutexEmulation::Wake -> NotifyAsyncWaiter posts a + // ResolveAsyncWaiterPromisesTask to the isolate's foreground runner (B's + // worker thread). B picks the task up but blocks on the v8::Locker (held by + // A's InitV8). The .then() handler closes over A's `alert`, whose bound + // v8::External holds a bare (non-raw_ptr) pointer to A's C++ Context. + scoped_refptr script_a = net::PacFileData::FromUTF8( + "var ia = new Int32Array(new SharedArrayBuffer(8));\n" + "Atomics.waitAsync(ia, 0, 0).value.then(function(r){ alert(r); });\n" + "Atomics.notify(ia, 0, 1);\n" + "function FindProxyForURL(url, host) { return 'DIRECT'; }\n"); + std::unique_ptr resolver_a = + CreateResolverWithScriptData(mock_bindings_a.CreateBindings(), script_a); + ASSERT_TRUE(mock_bindings_a.GetErrors().empty()) + << "script_a top-level threw; SAB/Atomics may be unavailable"; + + // Once A's InitV8 releases the Locker, B runs the + // ResolveAsyncWaiterPromisesTask: it enters A's NativeContext, calls + // JSPromise::Resolve("ok"), and enqueues a PromiseFulfillReactionJobTask in + // the isolate's default kAuto MicrotaskQueue. RunWithLocker does not pump + // microtasks, so the reaction sits in the shared queue. + + // Step 4: Destroy resolver A. ~Context acquires the Locker, calls + // ContextDisposedNotification (which does not touch the futex list or the + // microtask queue), Reset()s its persistents, and frees the C++ Context. + // The PromiseFulfillReactionJobTask in the shared microtask queue still + // strongly roots A's NativeContext and its `alert` Function, whose External + // now dangles. + resolver_a.reset(); + + // Step 5: Unpark B. OnDnsOperationComplete posts ExecuteNonBlocking back to + // B's worker. ResolveProxy -> v8::Function::Call runs FindProxyForURL (both + // dnsResolve()s now hit cache) and returns to call-depth 0. ~CallDepthScope + // -> FireCallCompletedCallback pumps the shared kAuto microtask queue, which + // runs A's reaction job in A's NativeContext and invokes alert("ok"). + // AlertCallback then does + // static_cast(External->Value(tag))->js_bindings()->Alert(...) + // through the freed Context allocation -> heap-use-after-free + virtual call. + host_resolver.ResolveSecond(); + EXPECT_THAT(callback_b.WaitForResult(), IsOk()); +} + } // namespace } // namespace proxy_resolver