820623987e34d2f4b31621270a72f6ecb79356cb [M144-LTS] prevent CommitDeferringConditionRunner from potential UAF
The bug at a glance
The commit message and added CHECK/weak-pointer guards describe a genuine use-after-free of the NavigationRequest-owned CommitDeferringConditionRunner in the privileged browser process, reachable when a condition resumes synchronously; it is an M144-LTS backport, signaling real security impact.
A renderer or other actor able to drive a navigation whose commit deferring condition resumes synchronously (e.g. via a synchronous Mojo call) can cause the runner to re-enter freed state, giving a browser-process UAF that is a strong sandbox-escape primitive.
Root cause
ResumeProcessing() was designed to be posted asynchronously, but a synchronous Mojo reply can make a condition invoke its resume_closure during WillCommitNavigation(), re-entering ResumeProcessing() while ProcessConditions() is still on the stack. That nested processing can delete an earlier condition (or the whole NavigationRequest that owns the runner); when the stack unwinds to the outer ProcessConditions(A), it dereferences the already-freed condition/runner. The fix adds an is_in_will_commit_navigation_ guard so a synchronous ResumeProcessing() only sets was_resumed_synchronously_ and returns, captures a weak_self around the WillCommitNavigation() call to detect runner destruction, and treats a synchronous resume of a kDefer result as kProceed.
Key code
Reentrancy guard to prevent synchronous re-entry UAF
void CommitDeferringConditionRunner::ResumeProcessing() {
if (is_in_will_commit_navigation_) {
was_resumed_synchronously_ = true;
return;
}
Patch walkthrough
content/browser/renderer_host/commit_deferring_condition_runner.cc— ResumeProcessing() now bails out early (setting was_resumed_synchronously_) if it is called while a WillCommitNavigation() is in progress, preventing nested teardown. The WillCommitNavigation() call is wrapped with a weak_self capture and a post-call liveness check (CHECK_NE kProceed when the runner was deleted), and the kDefer path treats a synchronous resume as kProceed instead of marking is_deferred_. This removes the window where a freed condition/runner is used after an earlier condition or the owning NavigationRequest is destroyed.content/browser/renderer_host/commit_deferring_condition_runner.h— Adds is_in_will_commit_navigation_ and was_resumed_synchronously_ state and updates the ResumeProcessing() comment to reflect that it may now run synchronously, documenting the reentrancy contract.content/browser/renderer_host/commit_deferring_condition_runner_unittest.cc— Adds regression tests: BasicSyncResume (condition that runs resume_closure synchronously then returns kDefer) and DeleteRunnerDuringCall (condition deletes the runner during WillCommitNavigation and returns kCancelled), confirming the new guards handle synchronous resume and self-destruction.
Background
CommitDeferringConditionRunner — A browser-process object owned by a NavigationRequest that iterates a list of conditions that can defer a navigation commit until each is satisfied.
WillCommitNavigation / resume_closure — Each condition is handed a resume_closure to call (normally asynchronously) when it is ready; returning kDefer means the runner waits for that closure.
Synchronous Mojo call — A [Sync] Mojo method can run a reply/closure synchronously on the same stack, violating the asynchronous assumption behind ResumeProcessing().
WeakPtrFactory liveness check — weak_factory_.GetWeakPtr() lets the runner detect that WillCommitNavigation() destroyed the NavigationRequest that owns it before touching any member.
Vulnerability window
- Reachability — An attacker drives a navigation whose commit involves a deferring condition that completes via a synchronous Mojo reply (inferred as to the exact condition).
- Trigger — The condition calls resume_closure synchronously inside WillCommitNavigation(), re-entering ResumeProcessing()/ProcessConditions() before the outer call returns.
- Primitive — Nested processing frees an earlier condition or the owning NavigationRequest, and the unwinding outer frame dereferences freed memory, yielding a browser-process use-after-free.
- Escalation — Controlled reallocation of the freed region could turn the UAF into type confusion or control-flow hijack in the browser process (inferred).
Proof of concept
Conceptually, the bug is triggered by a navigation that reaches a CommitDeferringCondition which resolves synchronously (invokes its resume_closure before returning) while another condition’s teardown frees the runner; reproduction requires a condition whose completion path is synchronous, as modeled by the added SynchronousResumeCondition/DeletingCondition test doubles. No exploit code is provided.
Exploitation
- Setup — Arrange a navigation that engages a commit deferring condition whose resume path can fire synchronously (inferred).
- Trigger reentrancy — Cause the synchronous resume so ResumeProcessing() re-enters ProcessConditions() and frees an in-use condition/runner.
- Groom — Reclaim the freed allocation with attacker-influenced data before the stale pointer is used (inferred).
- Gain control — Leverage the dangling dereference for info leak or control-flow corruption in the browser process (inferred).
Detection & hunting
For defenders and detection engineers:
- weggli/grep for closures invoked synchronously inside WillCommitNavigation overrides — Audit CommitDeferringCondition subclasses for any that may Run() the resume_closure before returning, which is the precondition this fix defends against.
- grep for ‘DO NOT ADD CODE’ / weak_self checks around WillCommitNavigation in content/browser/renderer_host — Confirm the reentrancy guards are present on the LTS branch and not reverted; absence indicates a regression window.
- ASAN/UAF crash reports referencing CommitDeferringConditionRunner::ProcessConditions — Hunt crash dumps for use-after-free frames in this runner during navigation commit.
Audit directions
- All CommitDeferringCondition implementations in content/ — Review each WillCommitNavigation override for synchronous resume or self-destruction behavior that could re-enter the runner.
- Other async-assumed callback runners in the navigation stack — Look for similar patterns where an object posts a resume callback but a synchronous Mojo reply can invoke it inline, re-entering an iteration loop.