← Chrome — 2026-W40

f97c3791aa819d117a910ca935ea70d87d2aa661  [M144-LTS] prevent CommitDeferringConditionRunner from potential UAF

severity high class UAF confidence 0.93 signal score 15.26 content/browser/ patch-now AI review CONFIRMED
Primitive: synchronous ResumeProcessing re-enters ProcessConditions and frees a condition/NavigationRequest still referenced by an outer frame (UAF)
Triage note: M144-LTS 'prevent ... potential UAF'; adds is_in_will_commit_navigation_/was_resumed_synchronously_ guards + a weak_self WeakPtr around WillCommitNavigation. Synchronous Mojo makes ResumeProcessing fire inline during commit; renderer-reachable, confirmed by regression tests.
Structural signals: smartptr_member_added (+6.47), null_guard (+2.59), uaf_lifetime (+2.2), msg_strong (+3.0), area:content/browser/ (+1.0)

The bug at a glance

Use-after-free of a NavigationRequest in content/browser’s CommitDeferringConditionRunner, which runs in the browser process during navigation commit; a synchronous Mojo reply can re-enter and free the object mid-iteration. Backported to M144-LTS, consistent with a High-severity browser-process navigation UAF.

Commit-deferring conditions call back into (potentially compromised) renderers over Mojo; if a synchronous Mojo call makes a condition resume or cancel the navigation synchronously, the runner frees a NavigationRequest that outer frames still dereference, turning the commit pipeline into a UAF reachable from a malicious renderer.

Root cause

CommitDeferringConditionRunner::ResumeProcessing was designed to be invoked asynchronously after a condition defers, but a synchronous Mojo call can invoke it synchronously from inside condition->WillCommitNavigation. With two conditions A and B, ProcessConditions can delete condition A, ResumeProcessing re-enters, A->WillCommitNavigation consumes its resume closure, and ProcessConditions(A) runs again; when control unwinds to the outer ProcessConditions(A) frame it finds A already freed — and more broadly WillCommitNavigation can destroy the NavigationRequest that owns the runner itself. The fix adds is_in_will_commit_navigation_ and was_resumed_synchronously_ flags: if ResumeProcessing fires while is_in_will_commit_navigation_ is true, it records was_resumed_synchronously_ and returns instead of recursing. ProcessConditions now takes a weak_self before the call and checks ‘!weak_self’ after — if the runner was destroyed it CHECK_NEs result against kProceed and returns.

Key code

commit_deferring_condition_runner.cc — detect synchronous resume and guard against self-destruction

void CommitDeferringConditionRunner::ResumeProcessing() {
  if (is_in_will_commit_navigation_) {
    was_resumed_synchronously_ = true;
    return;
  }
  DCHECK(is_deferred_);
  ...
}
// in ProcessConditions, around the condition call:
base::WeakPtr<CommitDeferringConditionRunner> weak_self =
    weak_factory_.GetWeakPtr();
Result result = condition->WillCommitNavigation(std::move(resume_closure));
if (!weak_self) {  // DO NOT ADD CODE before this check
  CHECK_NE(result, Result::kProceed);
  return;
}

Patch walkthrough

  • content/browser/renderer_host/commit_deferring_condition_runner.cc — ResumeProcessing gains an early-return: if is_in_will_commit_navigation_ it sets was_resumed_synchronously_ and returns, so a synchronous resume no longer recurses. In ProcessConditions, the direct WillCommitNavigation call is wrapped by capturing weak_self before and checking ‘!weak_self’ after (runner destroyed -> CHECK_NE result, kProceed; return). The kDefer case now breaks early when was_resumed_synchronously_ is set.
  • content/browser/renderer_host/commit_deferring_condition_runner.h — Adds two bool members: is_in_will_commit_navigation_ and was_resumed_synchronously_. The ResumeProcessing doc comment is updated to drop the ‘Called asynchronously’ claim.
  • content/browser/renderer_host/commit_deferring_condition_runner_unittest.cc — Adds BasicSyncResume (SynchronousResumeCondition runs the resume closure synchronously then returns kDefer; runner must proceed without deferring) and DeleteRunnerDuringCall (DeletingCondition deletes the runner inside WillCommitNavigation and returns kCancelled, exercising the weak_self path). Regression test for crbug.com/502293787.

Background

CommitDeferringConditionRunner — content/browser object, owned by a NavigationRequest, that iterates a list of CommitDeferringConditions before a navigation commits, deferring when a condition is not yet ready.

WillCommitNavigation / resume_closure — Each condition’s WillCommitNavigation returns kProceed/kDefer/kCancelled and is handed a resume closure (bound to ResumeProcessing via a weak ptr) to call when it later becomes ready.

Synchronous Mojo invocation — ResumeProcessing was assumed async, but a synchronous Mojo call can drive a condition to run its resume closure (or cancel the navigation) synchronously inside WillCommitNavigation, causing re-entrancy.

NavigationRequest ownership — The runner is owned by the NavigationRequest; a condition that cancels the navigation can destroy the NavigationRequest and thus the runner mid-call, so post-call member access is unsafe.

Vulnerability window

  1. Original fix — Landed on main as commit 5f51bc193d29 (CL 7867471).
  2. LTS backport — Cherry-picked as f97c3791aa (dated 2026-09-28) to the M144-LTS branch, CL 8335387.
  3. Tracking — Regression tests BasicSyncResume and DeleteRunnerDuringCall reference crbug.com/502293787.

Proof of concept

Grounded in the two regression tests. SynchronousResumeCondition runs the resume closure synchronously then returns kDefer; DeletingCondition destroys the runner inside WillCommitNavigation and returns kCancelled. On the unpatched runner, the first recurses into ResumeProcessing and the second leaves outer frames using a freed runner/NavigationRequest. The real-world trigger — a synchronous Mojo call from a renderer driving a condition to resume or cancel synchronously — is (inferred).

// Condensed from the diff's regression tests
Result WillCommitNavigation(base::OnceClosure resume_closure) override {
  std::move(resume_closure).Run();   // resume synchronously
  return Result::kDefer;
}
// delete runner during the call:
Result WillCommitNavigation(base::OnceClosure resume_closure) override {
  std::move(delete_runner_callback_).Run();  // runner_.reset()
  return Result::kCancelled;
}

Exploitation

  1. Reach — Drive a navigation through conditions that call back into the renderer over Mojo while the runner is iterating.
  2. Re-enter — (inferred) Use a synchronous Mojo call so a condition runs its resume closure or cancels the navigation synchronously inside WillCommitNavigation, re-entering ProcessConditions or deleting the runner.
  3. Free / use — The re-entrant path frees condition A or the NavigationRequest that owns the runner while the outer ProcessConditions frame still references it.
  4. Escalate — (inferred) Reclaiming the freed NavigationRequest/condition allocation and converting the browser-process UAF into controlled corruption or a sandbox escape is not demonstrated by the patch.

Detection & hunting

For defenders and detection engineers:

  • weak_self CHECK_NE firing — Post-patch, a crash on CHECK_NE(result, kProceed) after a destroyed runner indicates a condition proceeded while also destroying the navigation.
  • UAF in ProcessConditions — On ASAN builds, flag heap-use-after-free with CommitDeferringConditionRunner::ProcessConditions / ResumeProcessing / NavigationRequest on the stack during commit.
  • Synchronous Mojo during commit — Audit/trace commit-deferring conditions that issue synchronous Mojo calls to renderers; unusual synchronous resume/cancel during navigation commit is the behavioral precondition.

Audit directions

  • Async-assumed callbacks invoked synchronously — Search content/browser for methods documented as ‘called asynchronously’ that can be reached synchronously via Mojo or re-entrant closures; add in-progress flags or weak-ptr guards.
  • Post-callback member access in navigation — Review all CommitDeferringCondition implementations and other NavigationRequest-owned iterators for code that touches this/members after a call that can destroy the NavigationRequest.
  • Condition list mutation during iteration — Check that deleting or resuming a condition from inside WillCommitNavigation cannot invalidate the conditions_ iteration in ProcessConditions.

Before / after

Loading diff…