High chrome Race 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Headless
DescriptionImproper input validation in Headless
ComponentHeadless
Sub-componentHeadless
Bug ClassRace
Tracker572111820
Fix commita6bfd9e61f16 (chromium/src) +7/-9
CISA KEVNot listed
CreditedAnonymous
Disclosed2026-10-27

Background

`base::CommandLine::ForCurrentProcess()`
a process-global singleton holding the parsed command-line switches. Its switch map is read concurrently from many threads via HasSwitch() and is intended to be immutable once worker threads start.
Headless command mode
Chrome’s --headless command execution (screenshot/PDF/DOM dump). headless::ProcessHeadlessCommands() orchestrates running those commands during browser startup.
`switches::kDisableLazyLoading`
a switch consulted by ChromeContentBrowserClient::OverrideWebPreferences() to decide web_prefs->lazy_load_enabled, used so headless captures include lazily-loaded content.
`headless::ShouldProcessHeadlessCommands()`
a pure predicate reporting whether the current launch is executing headless commands, usable without mutating global state.
`OverrideWebPreferences()`
called on the UI thread per web-preferences update to populate web_prefs.

Root Cause Analysis

The advisory labels this “Improper input validation in Headless” (InputValidation), but the diff and commit message describe a data race on process-global mutable state — there is no input parsing or validation in the change. Previously ProcessHeadlessCommands() reached into base::CommandLine::ForCurrentProcess() and, at runtime during browser startup, called command_line->AppendSwitch(switches::kDisableLazyLoading). By that point worker threads had already started and were concurrently calling HasSwitch() on the same CommandLine instance. base::CommandLine is not thread-safe for concurrent write/read: mutating its internal switch container while other threads iterate/look it up is a classic read/write data race, with undefined behavior ranging from a missed/duplicated switch read to container corruption.

The fix removes the runtime mutation entirely. ProcessHeadlessCommands() no longer appends the switch (and drops the chrome/common/chrome_switches.h include and the //chrome/common:non_code_constants dependency). Instead, OverrideWebPreferences() now disables lazy loading by OR-ing the existing kDisableLazyLoading switch check with a direct call to headless::ShouldProcessHeadlessCommands() (guarded by #if !BUILDFLAG(IS_ANDROID)). This moves the decision to a read-only predicate evaluated where it is needed, so the global CommandLine is never written after threads start and the concurrent HasSwitch() readers are no longer racing a writer.

Key insight
Headless startup wrote a new switch into the process-global base::CommandLine after worker threads were already reading it with HasSwitch(), a write/read data race; the fix replaces the mutation with a read-only ShouldProcessHeadlessCommands() check at the single consumption site.

Attack Path

  1. Precondition Chrome launched in headless-command mode (ProcessHeadlessCommands() runs) without --disable-lazy-loading already present, so the append path executes. This is a launch/configuration condition, not a web-reachable one (inferred: not attacker-triggered from content).
  2. Startup window worker/background threads start and begin calling CommandLine::HasSwitch() concurrently with the startup sequence.
  3. Concurrent write ProcessHeadlessCommands() calls AppendSwitch(kDisableLazyLoading), mutating the shared switch container while readers iterate it.
  4. Race outcome the unsynchronized read/write yields undefined behavior: inconsistent switch reads or container memory corruption.
  5. Primitive a data race (potential memory corruption / inconsistent configuration) in the browser process; it is timing-dependent and not shown by the diff to be externally controllable.

Impact Assessment

The race is in the privileged browser process during startup of a headless invocation. It is not reachable from web content; triggering it requires launching Chrome in headless-command mode, so the practical threat surface is local/automation contexts rather than a remote attacker. The diff substantiates undefined behavior from concurrent base::CommandLine write/read; whether it is reliably exploitable beyond flaky switch state or a crash is not shown (inferred). The severity rating (“high”) likely reflects the possibility of switch-map corruption, but the diff only proves the race, not a concrete memory-corruption primitive.

Files Changed

  • chrome/browser/headless/BUILD.gn
  • chrome/browser/BUILD.gn
  • chrome/browser/headless/headless_command_processor.cc
  • chrome/browser/chrome_content_browser_client.cc

Audit Directions

  • Runtime `CommandLine` mutation
    grep for AppendSwitch/AppendSwitchASCII on ForCurrentProcess() executed after startup/thread creation; each is a potential race with HasSwitch() readers.
  • Global singletons mutated post-startup
    review other process-global config objects (feature lists, field trials) for late writes concurrent with background-thread reads.
  • Headless-only side effects
    check other places ProcessHeadlessCommands() or headless setup alters shared state, and whether those can be converted to read-only predicates like ShouldProcessHeadlessCommands().
  • Advisory mislabel
    “Improper input validation” does not match a threading bug; audit sibling Headless CVEs for similarly generic classifications.
From a6bfd9e61f164111f2ac5dffc340306b9993d002 Mon Sep 17 00:00:00 2001
From: Peter Kvitek <kvitekp@chromium.org>
Date: Thu, 15 Oct 2026 07:19:46 -0700
Subject: [PATCH] [headless] Do not mutate process CommandLine in ProcessHeadlessCommands

headless::ProcessHeadlessCommands() previously appended
--disable-lazy-loading switch to base::CommandLine::ForCurrentProcess()
during browser startup after worker threads had already started, causing
a data race with concurrent HasSwitch() reads on background threads.

This CL removes the runtime CommandLine mutation and instead checks
headless::ShouldProcessHeadlessCommands() directly in
ChromeContentBrowserClient::OverrideWebPreferences() to disable lazy
loading when executing headless commands.

Bug: 572111820
Change-Id: If3704bf958870216f6b85e57acac98a4b99ee0ee
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8582773
Commit-Queue: Peter Kvitek <kvitekp@chromium.org>
Reviewed-by: Lei Zhang <thestig@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1719643}
---
diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc
index 896701c5..35ce3c6e 100644
--- a/chrome/browser/chrome_content_browser_client.cc
+++ b/chrome/browser/chrome_content_browser_client.cc
@@ -582,6 +582,7 @@
 #include "chrome/browser/direct_sockets/chrome_direct_sockets_delegate.h"
 #include "chrome/browser/glic/public/features.h"
 #include "chrome/browser/global_features.h"
+#include "chrome/browser/headless/headless_command_processor.h"
 #include "chrome/browser/indigo/onboarding/indigo_onboarding_dialog.h"
 #include "chrome/browser/loader/features.h"
 #include "chrome/browser/loader/fetch_keepalive_process_manager.h"
@@ -4945,7 +4946,11 @@
   }
 
   if (base::CommandLine::ForCurrentProcess()->HasSwitch(
-          switches::kDisableLazyLoading)) {
+          switches::kDisableLazyLoading)
+#if !BUILDFLAG(IS_ANDROID)
+      || headless::ShouldProcessHeadlessCommands()
+#endif
+  ) {
     web_prefs->lazy_load_enabled = false;
   } else {
     web_prefs->lazy_load_enabled =
diff --git a/chrome/browser/headless/headless_command_processor.cc b/chrome/browser/headless/headless_command_processor.cc
index 31e9e12a..2baec9e8 100644
--- a/chrome/browser/headless/headless_command_processor.cc
+++ b/chrome/browser/headless/headless_command_processor.cc
@@ -9,7 +9,6 @@
 #include "base/command_line.h"
 #include "base/functional/bind.h"
 #include "chrome/browser/headless/headless_mode_util.h"
-#include "chrome/common/chrome_switches.h"
 #include "components/keep_alive_registry/keep_alive_types.h"
 #include "components/keep_alive_registry/scoped_keep_alive.h"
 #include "content/public/browser/browser_context.h"
@@ -29,12 +28,6 @@
     HeadlessCommandHandler::DoneCallback done_callback) {
   CHECK(browser_context, base::NotFatalUntil::M161);
 
-  // Ensure lazy loaded content is being captured by the commands.
-  base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
-  if (!command_line->HasSwitch(switches::kDisableLazyLoading)) {
-    command_line->AppendSwitch(switches::kDisableLazyLoading);
-  }
-
   auto keepalive = std::make_unique<ScopedKeepAlive>(
       KeepAliveOrigin::HEADLESS_COMMAND, KeepAliveRestartOption::DISABLED);
 
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 9bb7c6a..406d170 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -1489,6 +1489,7 @@
       "//chrome/browser/dictation",
       "//chrome/browser/direct_sockets",
       "//chrome/browser/error_reporting",
+      "//chrome/browser/headless:command_processor",
       "//chrome/browser/password_manager/remote_actor",
       "//chrome/browser/ui:browser_actions_impl",
       "//chrome/browser/ui:browser_commands",
diff --git a/chrome/browser/headless/BUILD.gn b/chrome/browser/headless/BUILD.gn
index c26aa1d5..f5601a319 100644
--- a/chrome/browser/headless/BUILD.gn
+++ b/chrome/browser/headless/BUILD.gn
@@ -77,7 +77,6 @@
     deps = [
       ":headless",
       "//base",
-      "//chrome/common:non_code_constants",
       "//components/headless/command_handler",
       "//components/keep_alive_registry",
       "//content/public/browser",
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.