79286fe09f53c644667463b3b7e5f5a8988de161 Add CHECK in `StartLoadingInternal` to confirm nullptr usage.
Triage note: Adds a CHECK for an observed null body_loader_ (renderer null-deref/DoS during document load). Hardening/diagnostic rather than a root-cause fix; watch.
The bug at a glance
This is a diagnostic CHECK, not a functional fix. It converts a latent null-pointer method call in DocumentLoader::StartLoadingInternal into a deterministic, release-build CHECK failure so the root cause of crbug/471268403 can be pinned down. The pre-existing bug is a renderer-process null dereference: body_loader_ (a WebNavigationBodyLoader) is occasionally null when SetDefersLoading is invoked on it. WebNavigationBodyLoader::SetDefersLoading is a virtual interface method, so the call reads a vtable pointer from a near-null address and crashes. The worst realistic impact is a renderer-process crash (denial of service) reachable during navigation body loading; it does not cross a trust boundary and yields no attacker-controlled memory write. There is no known controlled-corruption primitive, and the commit itself concedes the trigger is not yet understood (‘is sometimes null’). Low severity, renderer-DoS-only. CVSS 5.3 reflects a bounded availability impact with no confidentiality/integrity effect.
body_loader_ is the DocumentLoader’s handle to the navigation response body loader. By the time StartLoadingInternal reaches the SetDefersLoading call it is expected to be a live object, yet crash telemetry (crbug/471268403) shows it is sometimes null. The interesting question is the lifecycle race that nulls it out before body loading is (re)started: this member is normally handed off/reset around commit, freeze-for-BFCache, and navigation cancellation. The CHECK is instrumentation to catch that state deterministically rather than a fix for the underlying sequencing bug.
Root cause
In StartLoadingInternal, immediately after InitializePrefetchedSignedExchangeManager(), the code unconditionally calls body_loader_->SetDefersLoading(freeze_mode_) to propagate the current freeze/defer state to the body loader. The patch inserts CHECK(body_loader_) directly before that dereference, with a comment referencing the tracking bug. This does not add a recovery path or an early return; it hardens the pre-existing implicit assumption into an explicit, always-on assertion. In release builds the CHECK will abort with a clean, greppable signature at a fixed line instead of the current opaque near-null vtable crash inside SetDefersLoading, giving triage a precise stack and confirming that body_loader_ (not some deeper pointer) is the null one.
Key code
Diagnostic CHECK inserted before the body_loader_ dereference in DocumentLoader::StartLoadingInternal
InitializePrefetchedSignedExchangeManager();
+ // https://crbug.com/471268403 implies that this is sometimes null.
+ CHECK(body_loader_);
body_loader_->SetDefersLoading(freeze_mode_);
}
Patch walkthrough
third_party/blink/renderer/core/loader/document_loader.cc— A single CHECK(body_loader_) is added in StartLoadingInternal just before body_loader_->SetDefersLoading(freeze_mode_), with a comment pointing at crbug/471268403. It changes nothing functionally on the happy path; on the fault path it turns an implicit null dereference of a virtual method call into an explicit CHECK failure. Note this is a CHECK, not a DCHECK, so it fires in shipping Chrome, converting a fuzzy production crash into a deterministic, attributable one to advance root-cause analysis.
Background
DocumentLoader — Blink’s per-navigation object that owns the response pipeline for a document: it drives body loading, parsing kickoff, and commit. StartLoadingInternal is the entry point that begins consuming the response body.
body_loader_ / WebNavigationBodyLoader — The interface object that streams the navigation response body to the renderer. It is created/handed to DocumentLoader by the navigation machinery and can be reset or taken during commit, cancellation, or BFCache freeze. SetDefersLoading is a virtual method, so calling it through a null pointer crashes on the vtable read.
freeze_mode_ / SetDefersLoading — SetDefersLoading pauses or resumes network/body delivery. freeze_mode_ encodes the current freeze state (e.g. for Back/Forward Cache or pause-for-pending states). StartLoadingInternal propagates it into the body loader so newly started loading respects the existing freeze.
CHECK vs DCHECK — CHECK is compiled into release builds and aborts the process on failure; DCHECK only runs in debug/dev builds. Choosing CHECK here means the assertion actively guards production and generates crash reports usable for triage, at the cost of turning would-be null-deref crashes into intentional aborts.
InitializePrefetchedSignedExchangeManager — Runs just before the guarded call; sets up handling for prefetched Signed Exchange (SXG) resources. Its presence marks the exact position in StartLoadingInternal where the body loader is expected to already exist.
Vulnerability window
- Discovery — crbug/471268403 accumulated crash reports showing a null dereference reaching body_loader_->SetDefersLoading in StartLoadingInternal. The trigger sequence was not reproduced deterministically.
- Instrumentation — On 2026-01-15 Fergal Daly landed CL 7400925 (r1570204) adding CHECK(body_loader_) to confirm the hypothesis that body_loader_ itself is the null pointer, rather than fixing the underlying nulling race.
- Follow-up (expected) — Once the CHECK yields deterministic reports pinpointing the caller sequence, a real fix (guarding the call site, or fixing whatever resets body_loader_ before this path) is expected to follow. This commit is explicitly a watch-tier diagnostic step.
Proof of concept
No test or reproducer shipped with this commit; the commit is instrumentation whose whole purpose is to discover the trigger, which was unknown at landing time. An honest recipe: exercise navigation paths that reset/take body_loader_ around commit, cancellation, or BFCache freeze and then re-enter StartLoadingInternal (for example, a navigation that is frozen or defer-toggled while the body loader has already been moved out). Because the exact sequencing is unknown per the commit, no reliable reproducer can be stated, and no exploit is demonstrated. The only outcome is a renderer-process crash/CHECK abort.
Exploitation
- Reach — Requires driving a same-renderer navigation into the specific lifecycle state where body_loader_ has been nulled before StartLoadingInternal propagates the freeze mode. This is a timing/sequencing condition, not a directly parameterizable input.
- Primitive — A virtual call through a null pointer: reads a vtable slot at a small offset from address 0 and jumps, which faults. No attacker-controlled value flows into the dereference; there is no write of controlled data.
- Impact — Renderer-process crash only (post-patch: a deterministic CHECK abort). Not a sandbox escape and not memory-corruption suitable for control-flow hijack under normal near-null protections.
Detection & hunting
For defenders and detection engineers:
- CHECK failure signature — After this CL, watch crash dashboards for CHECK failures at document_loader.cc in StartLoadingInternal with message body_loader_. That is the intended detection channel and confirms the null-handle hypothesis.
- Pre-patch crash clustering — Historic reports show near-null vtable-read crashes inside WebNavigationBodyLoader::SetDefersLoading called from DocumentLoader::StartLoadingInternal; cluster these under crbug/471268403.
Audit directions
- body_loader_ lifecycle — Trace every assignment, reset, and move of body_loader_ in document_loader.cc (creation, TakeBodyLoader-style handoffs, commit, StopLoading/cancellation). Identify the path that clears it before StartLoadingInternal runs, especially re-entrant or asynchronous continuations.
- Freeze/BFCache sequencing — Review how SetDefersLoading/freeze_mode_ is applied across freeze-for-BFCache and pause states; a freeze delivered after the loader is torn down but before re-start is a plausible cause.
- Sibling call sites — Audit other unconditional body_loader_-> dereferences in DocumentLoader for the same missing-null assumption, since a race that nulls it here likely exposes those too.