CVE-2026-118336
Overview
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
--headlesscommand execution (screenshot/PDF/DOM dump).headless::ProcessHeadlessCommands()orchestrates running those commands during browser startup. - `switches::kDisableLazyLoading`
- a switch consulted by
ChromeContentBrowserClient::OverrideWebPreferences()to decideweb_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.
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
- Precondition
Chrome launched in headless-command mode (
ProcessHeadlessCommands()runs) without--disable-lazy-loadingalready present, so the append path executes. This is a launch/configuration condition, not a web-reachable one (inferred: not attacker-triggered from content). - Startup window
worker/background threads start and begin calling
CommandLine::HasSwitch()concurrently with the startup sequence. - Concurrent write
ProcessHeadlessCommands()callsAppendSwitch(kDisableLazyLoading), mutating the shared switch container while readers iterate it. - Race outcome the unsynchronized read/write yields undefined behavior: inconsistent switch reads or container memory corruption.
- 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
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.gnchrome/browser/BUILD.gnchrome/browser/headless/headless_command_processor.ccchrome/browser/chrome_content_browser_client.cc
Audit Directions
- Runtime `CommandLine` mutationgrep for
AppendSwitch/AppendSwitchASCIIonForCurrentProcess()executed after startup/thread creation; each is a potential race withHasSwitch()readers. - Global singletons mutated post-startupreview other process-global config objects (feature lists, field trials) for late writes concurrent with background-thread reads.
- Headless-only side effectscheck other places
ProcessHeadlessCommands()or headless setup alters shared state, and whether those can be converted to read-only predicates likeShouldProcessHeadlessCommands(). - Advisory mislabel“Improper input validation” does not match a threading bug; audit sibling Headless CVEs for similarly generic classifications.
Patch
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",