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 <[email protected]> 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 <[email protected]> Reviewed-by: Lei Zhang <[email protected]> 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",