installer: Add bootstrap installer for Windows (see #4093)

Add a shared CEF installer that downloads, verifies, and manages CEF
binary distributions on Windows. The installer is embedded in
application bootstrap binaries and resolves the correct CEF version
from a CDN manifest based on each application's version and ABI
compatibility requirements.

Key components:
- CDN manifest parsing and version resolution
- WinHTTP-based download with SHA-1/SHA-256 integrity verification
- Authenticode catalog (.cat) signature verification
- Parallel XZ decompression for fast archive extraction
- Per-version metadata (cef_version.json) with full version tracking
- Shared installation database for multi-app version management
- Version pruning to reclaim disk space from unused versions
- Revocation list support (compiled-in baseline + CDN delta)
- Progress UI with WM_COPYDATA-based cancellation
- Bundled CEF fallback for offline/CDN-failure scenarios
This commit is contained in:
Marshall Greenblatt committed 2026-07-27 14:04:48 -04:00
1 parent 7324f016a3
commit be20927a8d
202 files changed
+65465 -142

No files matched your search

+44 -9
View File
@@ -333,6 +333,7 @@ group("cef") {
":cefsimple_dll",
":cefsimple_capi_dll",
":ceftests_dll",
"libcef_dll/bootstrap/installer:installer_tests",
]
}
@@ -383,6 +384,7 @@ if (is_win) {
# Required by chrome_switches.cc
"//chrome/common:buildflags",
"//extensions/buildflags:buildflags",
"//printing/buildflags:buildflags",
"//ui/base:buildflags",
@@ -452,6 +454,33 @@ test("libcef_static_unittests") {
]
}
if (is_win) {
# Certificate utilities used by installer_signature and bootstrap.
# Separated to avoid duplicate symbol errors when both depend on these.
source_set("cef_certificate_util") {
sources = [
"include/wrapper/cef_certificate_util_win.h",
"include/wrapper/cef_util_win.h",
"libcef_dll/wrapper/cef_certificate_util_win.cc",
"libcef_dll/wrapper/cef_util_win.cc",
]
deps = [ "//base" ]
libs = [
"wintrust.lib",
"crypt32.lib",
]
defines = [ "CEF_BUILD_BOOTSTRAP" ]
configs += [
"libcef/features:config",
"//build/config:precompiled_headers",
]
}
}
# Source files that are linked into libcef and cef_framework and implement
# interfaces exposed to ceftests via the include/test directory. These sources
# may access Chromium/CEF internals. This is defined as a separate target from
@@ -1319,15 +1348,11 @@ if (is_mac) {
if (is_win) {
bootstrap_sources = includes_common +
includes_win + [
"include/wrapper/cef_certificate_util_win.h",
"include/wrapper/cef_util_win.h",
"libcef_dll/bootstrap/bootstrap_util_win.cc",
"libcef_dll/bootstrap/bootstrap_util_win.h",
"libcef_dll/bootstrap/bootstrap_win.cc",
"libcef_dll/bootstrap/win/bootstrap.rc",
"libcef_dll/bootstrap/win/resource.h",
"libcef_dll/wrapper/cef_certificate_util_win.cc",
"libcef_dll/wrapper/cef_util_win.cc",
"libcef/browser/crashpad_runner.cc",
"libcef/browser/crashpad_runner.h",
"libcef/browser/preferred_stack_size_win.inc",
@@ -1337,12 +1362,12 @@ if (is_win) {
"//chrome/common/win/delay_load_failure_support.h",
"//content/app/sandbox_helper_win.cc",
"//content/public/app/sandbox_helper_win.h",
]
bootstrap_deps = [
":make_api_versions_header",
":make_config_header",
":make_version_header",
":cef_certificate_util",
"libcef_dll/bootstrap/installer:cef_installer",
"//base",
"//build/win:default_exe_manifest",
"//chrome/install_static:secondary_module",
@@ -1373,7 +1398,12 @@ if (is_win) {
testonly = true
sources = bootstrap_sources
deps = bootstrap_deps
deps = bootstrap_deps + [
# Trigger generation of installer_config.json/rc so that the files exist
# for distributions (automate-git.py builds bootstrap but not cefclient_dll).
# The files are not embedded in this binary.
"libcef_dll/bootstrap/installer:cef_installer_config"
]
libs = bootstrap_libs
configs += bootstrap_configs
@@ -2550,10 +2580,15 @@ if (is_mac) {
gypi_paths2.cefclient_sources_common +
gypi_paths2.cefclient_sources_renderer +
gypi_paths2.cefclient_sources_win +
gypi_paths2.cefclient_sources_resources_win_rc
gypi_paths2.cefclient_sources_resources_win_rc + [
# Generated installer config resource (embedded in client DLL,
# read by bootstrap.exe).
"$target_gen_dir/installer_config.rc",
]
deps = [
":bootstrap",
"libcef_dll/bootstrap/installer:cef_installer_config",
":libcef",
":libcef_dll_wrapper",
]
+2
View File
@@ -368,6 +368,8 @@
'tests/cefclient/browser/browser_window_osr_win.h',
'tests/cefclient/browser/browser_window_std_win.cc',
'tests/cefclient/browser/browser_window_std_win.h',
'tests/cefclient/browser/client_update_win.cc',
'tests/cefclient/browser/client_update_win.h',
'tests/cefclient/browser/main_context_impl_win.cc',
'tests/cefclient/browser/main_message_loop_multithreaded_win.cc',
'tests/cefclient/browser/main_message_loop_multithreaded_win.h',
+79 -1
View File
@@ -85,6 +85,52 @@ typedef struct _cef_version_info_t {
///
char sandbox_compat_hash[17];
#endif
#if CEF_API_ADDED(CEF_NEXT)
///
/// Path to libcef.dll provided by the bootstrap installer (Windows only).
/// Null if not using the installer or on other platforms.
/// Owned by the bootstrap executable, valid for the lifetime of the process.
/// Always nullptr when populated via cef_version_info_all().
///
const wchar_t* libcef_path;
///
/// Non-zero if libcef_path points to a bundled CEF directory (shipped with
/// the application) rather than a CDN-installed version. When non-zero,
/// libcef.dll may be signed with the client application's certificate rather
/// than the CEF distribution certificate.
/// Always zero when populated via cef_version_info_all().
///
int libcef_is_bundled;
///
/// Full version string from installer metadata (e.g.,
/// "150.0.1+gabc1234+chromium-150.0.7871.4"). Used to verify that the
/// loaded libcef.dll matches the version the installer resolved.
/// Null if not using the installer or on other platforms.
/// Owned by the bootstrap executable, valid for the lifetime of the process.
/// Always nullptr when populated via cef_version_info_all().
///
const char* libcef_version_full;
///
/// Structured installer startup error code. A resolved startup has a
/// non-null libcef_path and value zero. Installer-not-configured has a null
/// libcef_path and value zero. A recoverable resolution failure has a null
/// libcef_path and a non-zero value with installer_error_message populated.
/// Always zero when populated via cef_version_info_all().
///
int installer_error_code;
///
/// UTF-8 diagnostic for a recoverable installer resolution failure, or null
/// otherwise. Owned by bootstrap process-lifetime storage and must not be
/// freed or retained beyond that lifetime. Always nullptr when populated via
/// cef_version_info_all().
///
const char* installer_error_message;
#endif
} cef_version_info_t;
#if CEF_API_ADDED(14600)
@@ -97,6 +143,23 @@ typedef struct _cef_version_info_t {
sizeof(((cef_version_info_t*)0)->sandbox_compat_hash))
#endif
#if CEF_API_ADDED(CEF_NEXT)
///
/// Minimum struct size required to contain the installer startup error fields.
/// Used for version compatibility checks.
///
#define CEF_VERSION_INFO_SIZE_WITH_INSTALLER_ERROR \
(offsetof(cef_version_info_t, installer_error_message) + \
sizeof(((cef_version_info_t*)0)->installer_error_message))
///
/// Return the full CEF version string for the libcef library (e.g.,
/// "143.0.3+gcbafc77+chromium-143.0.7499.0"). The returned pointer is valid
/// for the lifetime of the process.
///
CEF_EXPORT const char* cef_version_full(void);
#endif
///
/// Return all CEF version information for the libcef library.
///
@@ -132,6 +195,20 @@ typedef struct _cef_version_info_t {
#define _CEF_POPULATE_SANDBOX_HASH(info)
#endif
// Helper macro for setting bootstrap-only installer fields. By default all are
// NULL/0. The bootstrap can override these after calling
// CEF_POPULATE_VERSION_INFO.
#if CEF_API_ADDED(CEF_NEXT)
#define _CEF_POPULATE_LIBCEF_PATH(info) \
(info)->libcef_path = NULL; \
(info)->libcef_is_bundled = 0; \
(info)->libcef_version_full = NULL; \
(info)->installer_error_code = 0; \
(info)->installer_error_message = NULL
#else
#define _CEF_POPULATE_LIBCEF_PATH(info)
#endif
///
/// Populate CEF version information for the client library.
///
@@ -145,7 +222,8 @@ typedef struct _cef_version_info_t {
(info)->chrome_version_minor = CHROME_VERSION_MINOR; \
(info)->chrome_version_build = CHROME_VERSION_BUILD; \
(info)->chrome_version_patch = CHROME_VERSION_PATCH; \
_CEF_POPULATE_SANDBOX_HASH(info)
_CEF_POPULATE_SANDBOX_HASH(info); \
_CEF_POPULATE_LIBCEF_PATH(info)
#ifdef __cplusplus
}
+16 -9
View File
@@ -206,15 +206,22 @@ class CefScopedLibraryLoader final {
///
/// Load the CEF library (libcef.dll) in the main process from the specified
/// absolute path. If libcef.dll is code signed then all signatures must be
/// valid. If |thumbprint| is a SHA1 hash (e.g. 40 character upper-case
/// hex-encoded value) then the primary signature must match that thumbprint.
/// If |allow_unsigned| is true and |thumbprint| is nullptr then libcef.dll
/// may be unsigned, otherwise it must be validly signed. Failure of code
/// signing requirements or DLL loading will result in a FATAL error and
/// application termination. If |version_info| is specified then the
/// libcef.dll version information must also match. Returns true if the load
/// succeeds. Usage must be protected by cef::logging::ScopedEarlySupport.
/// absolute path. The library is loaded using LOAD_WITH_ALTERED_SEARCH_PATH,
/// which ensures that libcef.dll's dependencies are loaded from the same
/// directory as libcef.dll rather than from the application directory. If
/// libcef.dll is code signed then all signatures must be valid. If
/// |thumbprint| is a SHA1 hash (e.g. 40 character upper-case hex-encoded
/// value) then the primary signature must match that thumbprint. If
/// |allow_unsigned| is true and |thumbprint| is nullptr then libcef.dll may
/// be unsigned, otherwise it must be validly signed. Failure of code signing
/// requirements or DLL loading will result in a FATAL error and application
/// termination. If |version_info| is specified then the libcef.dll version
/// information must also match. Returns true if the load succeeds. Usage must
/// be protected by cef::logging::ScopedEarlySupport. Bootstrap clients
/// should size-check and handle installer_error_code and
/// installer_error_message before calling this method so they can select an
/// adjacent fallback or present cancellation guidance instead of immediately
/// terminating.
///
bool LoadInMainAssert(const wchar_t* dll_path,
const char* thumbprint,
+8
View File
@@ -60,6 +60,14 @@ std::vector<std::wstring> ParseCommandLineArgs(const wchar_t* str);
std::wstring GetCommandLineValue(const std::vector<std::wstring>& command_line,
const std::wstring& name);
///
/// Read binary data from a named RT_RCDATA resource.
/// Returns true if successful and populates |data|, false otherwise.
///
bool ReadResourceData(HMODULE module,
const wchar_t* resource_name,
std::string* data);
} // namespace cef_util
#endif // CEF_INCLUDE_WRAPPER_CEF_UTIL_WIN_H_
+84
View File
@@ -0,0 +1,84 @@
# CEF Bootstrap Contributor Guidance
The Windows bootstrap is a process-integration layer. Changes under
`installer/` must also follow [installer/AGENTS.md](installer/AGENTS.md) and
the authoritative documents referenced there.
## Directory ownership
- `bootstrap_win.cc` owns executable startup, process classification, sandbox
integration, module validation and loading, installer dispatch, and the
final handoff to the client DLL.
- `bootstrap_util_win.*` owns reusable module-name and module-path rules. It is
compiled into both the bootstrap executables and `libcef`; changes to its
switches, default names, or validation rules must remain compatible across
those consumers.
- `win/*` owns the bootstrap executable's Windows resources.
- `installer/*` owns installer behavior and has additional contributor
guidance in `installer/AGENTS.md`.
## Startup and security invariants
- Keep sandboxed subprocess startup separate from unsandboxed startup.
Sandboxed processes may already be locked down and must not gain calls to
Win32 APIs that are only safe before sandbox initialization.
- Preserve the client-DLL loading sequence: load without executing code,
validate the resolved path and signatures, allow installer inspection of
trusted resources, unload, and only then perform the normal executable load.
Never execute client DLL code from the untrusted load.
- Preserve fail-closed module-path, executable, client-DLL, and
`chrome_elf.dll` validation. Do not weaken these checks to support a new
launch layout; extend the trusted layout rules explicitly.
- Keep browser-process validation authoritative for sandboxed subprocesses.
Subprocesses consume the validated module name propagated by the browser
process and must not independently reinterpret untrusted path input.
## Compatibility and lifetime
- `bootstrap.exe` and `bootstrapc.exe` share `bootstrap_win.cc`. Keep their
behavior aligned except for intentional GUI/console entry-point differences.
- Changes to `bootstrap_util::switches::kModule` or the default bootstrap
executable names require corresponding `libcef` compatibility and rebuild
consideration.
- Keep installer startup state, version strings, launch-health data, and any
installed-version lease alive through the complete `RunWinMain` call.
- Preserve size-gated compatibility for structures passed across the
bootstrap/client DLL boundary. Update the corresponding version tests when
fields or ownership rules change.
## Installer boundary
- `bootstrap_win.cc` owns executable startup, process integration, CEF launch
orchestration, and calls through the public `cef_installer` interface.
- `bootstrap_win.cc` must not reference `cef_installer::internal` or
`cef_installer::test`. If it needs installer behavior, add or extend a
narrowly scoped public `cef_installer` operation that owns the complete
installer-specific decision.
- Do not promote a low-level helper solely to bypass this boundary. Keep
parsing, policy, validation, mutation, lifecycle, and testing-bypass
decisions inside the installer library.
- Bootstrap code may apply a test-only behavior only through a guarded public
installer operation. It must not manipulate an installer test seam directly.
- `bootstrap_win.cc` must not include an installer `*_test_support.h` header or
call an installer `*ForTesting` API.
Before finishing a change to `bootstrap_win.cc`, this command must produce no
matches:
```powershell
rg -n "cef_installer::(internal|test)::|installer/.*_test_support\.h|cef_installer::.*ForTesting" `
cef/libcef_dll/bootstrap/bootstrap_win.cc
```
## Documentation and verification
- [docs/sandbox_setup.md](../../docs/sandbox_setup.md) is the public reference
for bootstrap/client integration. Update it when client requirements,
exported entry points, module selection, or deployment layout changes.
- Build `cef`, `bootstrap`, and `bootstrapc` after changing shared bootstrap
code. Let Ninja determine the affected objects.
- Run `VersionTest.*` after changing bootstrap/client version information or
size-gated structures.
- Follow `installer/TESTING.md` for the required installer unit, integration,
and E2E coverage when installer dispatch, resolution, lifecycle, or
launch-health behavior changes.
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+701 -22
View File
@@ -4,15 +4,18 @@
#include <windows.h>
#include <algorithm>
#include <deque>
#include <iostream>
#include <optional>
#include <string_view>
#include "base/auto_reset.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/format_macros.h"
#include "base/functional/bind.h"
#include "base/hash/sha1.h"
#include "base/logging.h"
#include "base/no_destructor.h"
@@ -31,6 +34,16 @@
#include "cef/libcef/browser/crashpad_runner.h"
#include "cef/libcef/browser/preferred_stack_size_win.inc"
#include "cef/libcef_dll/bootstrap/bootstrap_util_win.h"
#include "cef/libcef_dll/bootstrap/installer/installer_bootstrap_helpers.h"
#include "cef/libcef_dll/bootstrap/installer/installer_config.h"
#include "cef/libcef_dll/bootstrap/installer/installer_controller.h"
#include "cef/libcef_dll/bootstrap/installer/installer_file_ops.h"
#include "cef/libcef_dll/bootstrap/installer/installer_launch_state.h"
#include "cef/libcef_dll/bootstrap/installer/installer_lifecycle.h"
#include "cef/libcef_dll/bootstrap/installer/installer_paths.h"
#include "cef/libcef_dll/bootstrap/installer/installer_progress_dialog.h"
#include "cef/libcef_dll/bootstrap/installer/installer_relaunch.h"
#include "cef/libcef_dll/bootstrap/installer/installer_signature.h"
#include "cef/libcef_dll/bootstrap/win/resource.h"
#include "chrome/app/delay_load_failure_hook_win.h"
#include "chrome/chrome_elf/chrome_elf_main.h"
@@ -262,6 +275,506 @@ struct DllLoadResult {
base::FilePath used_path;
};
// Creates a progress dialog and returns the progress callback that drives it.
// The caller must keep the returned unique_ptr alive for the duration of the
// operation. Returns nullptr dialog if show_progress_ui is false.
//
// The dialog is created but not shown until the progress callback receives a
// step indicating real work (CDN download or later). This avoids a brief
// dialog flash when a local/bundled CEF version resolves instantly.
std::pair<std::unique_ptr<cef_installer::ProgressDialog>,
cef_installer::ProgressCallback>
CreateProgressUI(const cef_installer::ExtendedConfig& extended) {
std::unique_ptr<cef_installer::ProgressDialog> dialog;
cef_installer::ProgressCallback callback;
if (extended.show_progress_ui) {
dialog =
std::make_unique<cef_installer::ProgressDialog>(extended.parent_window);
}
if (extended.show_progress_ui || extended.parent_window) {
auto shown = std::make_shared<bool>(false);
auto parent_cancel_pending = std::make_shared<bool>(false);
callback = base::BindRepeating(
[](cef_installer::ProgressDialog* dlg, HWND parent,
const std::shared_ptr<bool>& shown,
const std::shared_ptr<bool>& parent_cancel_pending,
cef_installer::Step step, uint64_t bytes_done,
uint64_t bytes_total) -> bool {
const bool committing = step == cef_installer::kStepCommitting;
if (dlg && !*shown && step >= cef_installer::kStepCdnResolve) {
dlg->Show();
*shown = true;
}
if (dlg) {
dlg->SetCancellationDeferred(committing);
dlg->SetStep(step);
int percent = cef_installer::CalculateOverallProgress(
step, bytes_done, bytes_total);
dlg->SetProgress(percent);
if (!committing && dlg->WasCancelled()) {
return false;
}
}
if (parent) {
const bool continue_requested = cef_installer::SendProgressToParent(
parent, step, bytes_done, bytes_total);
if (committing && !continue_requested) {
*parent_cancel_pending = true;
} else if (!committing &&
(!continue_requested || *parent_cancel_pending)) {
return false;
}
}
return true;
},
dialog.get(), extended.parent_window, shown, parent_cancel_pending);
}
return {std::move(dialog), std::move(callback)};
}
// Closes the progress dialog, showing an error dialog first on failure.
void CloseProgressUI(cef_installer::ProgressDialog* dialog,
const cef_installer::Result& result) {
if (!dialog) {
return;
}
if (!result.success &&
result.error_code != cef_installer::kExitCodeCancelled) {
dialog->ShowErrorDialog(result.error_code);
}
dialog->Close();
}
bool IsSuccessOrNeutralExitCode(int exit_code) {
switch (exit_code) {
case CEF_RESULT_CODE_NORMAL_EXIT:
case CEF_RESULT_CODE_PROFILE_IN_USE:
case CEF_RESULT_CODE_NORMAL_EXIT_PROCESS_NOTIFIED:
case CEF_RESULT_CODE_NORMAL_EXIT_PACK_EXTENSION_SUCCESS:
case CEF_RESULT_CODE_NORMAL_EXIT_AUTO_DE_ELEVATED:
return true;
default:
return false;
}
}
void WriteRetentionOutput(const cef_installer::Result& result,
bool json_output) {
std::string output =
cef_installer::FormatRetentionOutput(result, json_output);
HANDLE stdout_handle = ::GetStdHandle(STD_OUTPUT_HANDLE);
if (stdout_handle && stdout_handle != INVALID_HANDLE_VALUE &&
!cef_installer::WriteAllToHandle(stdout_handle, output)) {
PLOG(ERROR) << "Failed to write retention output";
}
}
// Run the installer for explicit commands (/cef-update, /cef-uninstall) or
// auto-install. Returns:
// - Exit code >= 0 if the process should exit (installer ran and completed)
// - -1 if the process should continue normally (no installer action taken)
// - Populates |startup_state| with configured/success/failure and launch
// health state owned through RunWinMain.
int MaybeRunInstaller(const base::CommandLine& command_line,
const base::FilePath& exe_path,
HMODULE client_dll_module,
cef_installer::InstallerStartupState* startup_state) {
const bool client_dll_exists = (client_dll_module != nullptr);
// Parse installer command from command line (/cef-update, /cef-uninstall)
std::optional<cef_installer::Command> explicit_command =
cef_installer::ParseInstallerCommand(command_line);
if (cef_installer::HasConflictingInstallerCommands(command_line)) {
LOG(ERROR) << "Conflicting explicit installer commands";
return cef_installer::kExitCodeConfigError;
}
// Establish the minimal trusted relaunch context before loading config so
// controlled child-side config failures can still report a terminal result.
auto uninstall_context = cef_installer::ResolveUninstallInvocationContext(
command_line, exe_path, explicit_command);
if (!uninstall_context) {
LOG(ERROR) << "Invalid or untrusted uninstall relaunch context";
return cef_installer::kExitCodeConfigError;
}
const cef_installer::UninstallInvocationState uninstall_invocation =
uninstall_context->invocation;
const cef_installer::UninstallRelaunchState& trusted_uninstall_state =
uninstall_context->relaunch_state;
const std::optional<cef_installer::UninstallLifecycleContext>&
lifecycle_context = trusted_uninstall_state.lifecycle_context;
cef_installer::Config config;
const cef_installer::ConfigLoadResult config_load =
cef_installer::TryLoadInstallerConfig(
client_dll_module, &config, nullptr,
uninstall_invocation ==
cef_installer::UninstallInvocationState::kRelaunched);
*startup_state =
cef_installer::MakeInstallerStartupState(config_load, config);
const bool retention_command =
explicit_command == cef_installer::Command::kRetentionDryRun ||
explicit_command == cef_installer::Command::kRetentionApply;
if (cef_installer::HasMisappliedRetentionOptions(command_line,
explicit_command)) {
LOG(ERROR) << "cef-max-age-days is valid only with a retention command";
return cef_installer::FinalizeControlledUninstallFailure(
lifecycle_context,
cef_installer::ControlledUninstallFailure::kInvalidRetentionOptions);
}
int retention_max_age_days = cef_installer::kDefaultRetentionMaxAgeDays;
if (retention_command && !cef_installer::ParseRetentionMaxAgeDays(
command_line, &retention_max_age_days)) {
LOG(ERROR) << "cef-max-age-days must be an integer from 90 through 3650";
return cef_installer::FinalizeControlledUninstallFailure(
lifecycle_context,
cef_installer::ControlledUninstallFailure::kInvalidRetentionAge);
}
// All installer paths require a config. Return early if none is available.
if (config_load.status == cef_installer::ConfigLoadStatus::kError) {
LOG(ERROR) << startup_state->error_message;
return cef_installer::FinalizeControlledUninstallFailure(
lifecycle_context,
cef_installer::ControlledUninstallFailure::kConfigLoad);
}
if (config_load.status == cef_installer::ConfigLoadStatus::kNotFound) {
if (explicit_command.has_value()) {
LOG(ERROR) << "Installer command requested but config not found";
return cef_installer::FinalizeControlledUninstallFailure(
lifecycle_context,
cef_installer::ControlledUninstallFailure::kConfigNotFound);
}
return -1;
}
if (explicit_command.has_value()) {
VLOG(1) << "Explicit installer command detected";
// Block explicit commands when enable_explicit_modes is not set.
// In non-official builds, allow explicit commands for development/testing.
#if defined(OFFICIAL_BUILD) && defined(NDEBUG)
if (!config.enable_explicit_modes) {
LOG(ERROR) << "Explicit installer commands require enable_explicit_modes";
return cef_installer::FinalizeControlledUninstallFailure(
lifecycle_context,
cef_installer::ControlledUninstallFailure::kExplicitModeDisabled);
}
#endif
// Set lower process priority for background mode (standalone installer
// only)
if (command_line.HasSwitch(cef_installer::kSwitchBackground)) {
SetPriorityClass(GetCurrentProcess(), BELOW_NORMAL_PRIORITY_CLASS);
}
}
// Resolve relative paths in config against the client DLL's directory.
// These fields are only parsed from the client DLL's embedded resource,
// so they will be empty in standalone mode (no client DLL).
if (client_dll_module &&
(!config.unchecked_cef_path.empty() || !config.bundled_cef_path.empty() ||
!config.install_path.empty())) {
wchar_t module_path_buf[MAX_PATH];
DWORD len =
::GetModuleFileNameW(client_dll_module, module_path_buf, MAX_PATH);
base::FilePath client_dll_dir;
if (len > 0 && len < MAX_PATH) {
client_dll_dir = base::FilePath(module_path_buf).DirName();
}
if (!config.unchecked_cef_path.empty()) {
if (client_dll_dir.empty()) {
config.unchecked_cef_path.clear();
} else {
base::FilePath resolved = cef_installer::ResolvePathRelativeTo(
config.unchecked_cef_path, client_dll_dir);
if (resolved.empty()) {
config.unchecked_cef_path.clear();
} else {
// Validate the resolved path for reparse points. Use the client
// DLL's directory as trust anchor when the path is under it (the
// common case, e.g., "."). For absolute paths outside the client
// DLL's directory, use the resolved directory's parent so that at
// minimum the directory and libcef.dll are checked.
base::FilePath libcef_candidate =
resolved.Append(cef_installer::kLibcefFilename);
base::FilePath trusted_root =
client_dll_dir.IsParent(libcef_candidate) ? client_dll_dir
: resolved.DirName();
if (base::PathExists(libcef_candidate) &&
!cef_installer::IsPathSafeForLoading(trusted_root,
libcef_candidate)) {
LOG(ERROR) << "Reparse point detected in unchecked_cef_path: "
<< libcef_candidate.value();
config.unchecked_cef_path.clear();
} else {
config.unchecked_cef_path = resolved.AsUTF8Unsafe();
VLOG(1) << "Resolved unchecked_cef_path to "
<< config.unchecked_cef_path;
}
}
}
}
if (!config.bundled_cef_path.empty()) {
if (client_dll_dir.empty()) {
config.bundled_cef_path.clear();
} else {
base::FilePath resolved = cef_installer::ResolvePathRelativeTo(
config.bundled_cef_path, client_dll_dir);
if (resolved.empty()) {
config.bundled_cef_path.clear();
} else {
config.bundled_cef_path = resolved.AsUTF8Unsafe();
VLOG(1) << "Resolved bundled_cef_path to " << config.bundled_cef_path;
}
}
}
if (!cef_installer::ResolveClientInstallPath(client_dll_module, &config)) {
LOG(ERROR) << "Failed to resolve client-resource install_path";
} else if (!config.install_path.empty()) {
VLOG(1) << "Resolved install_path to " << config.install_path;
}
}
// Parse extended config once, shared by all command paths below.
cef_installer::ExtendedConfig extended;
cef_installer::ParseExtendedConfigFromCommandLine(command_line, &extended);
extended.retention_max_age_days = retention_max_age_days;
// The selected application config supplies CDN URLs only when no
// operation-specific list or local mirror was provided.
if (extended.cdn_urls.empty() && extended.local_download_path.empty() &&
!config.cdn_urls.empty()) {
extended.cdn_urls = config.cdn_urls;
}
// Propagate bundled_cef_path from Config (client DLL resource) into
// ExtendedConfig so the controller's version selection considers it.
if (extended.bundled_cef_path.empty() && !config.bundled_cef_path.empty()) {
extended.bundled_cef_path = config.bundled_cef_path;
}
if (extended.install_path.empty() && !config.install_path.empty()) {
extended.install_path = config.install_path;
}
if (uninstall_invocation ==
cef_installer::UninstallInvocationState::kRelaunched) {
extended.install_path = trusted_uninstall_state.install_path;
}
cef_installer::ApplyConfigThumbprintOverride(config, &extended);
if (explicit_command == cef_installer::Command::kRetentionDryRun ||
explicit_command == cef_installer::Command::kRetentionApply) {
// Retention is terminal maintenance. It never registers the invoking app
// and never continues into resolved client-DLL execution.
extended.show_progress_ui = false;
cef_installer::Controller controller;
cef_installer::Result result =
controller.Run(*explicit_command, config, extended);
WriteRetentionOutput(
result, command_line.HasSwitch(cef_installer::kSwitchHeadless));
return cef_installer::ResultToExitCode(result);
}
if (explicit_command == cef_installer::Command::kUninstall) {
auto run_prepared_uninstall =
[&](const cef_installer::PreparedUninstall& prepared) {
auto [dialog, progress] = CreateProgressUI(extended);
cef_installer::Controller controller;
cef_installer::Result result = controller.RunPreparedUninstall(
config, extended, prepared, progress);
LOG(INFO) << "Uninstall completed: outcome="
<< cef_installer::OutcomeToString(result.outcome)
<< " success=" << result.success;
for (const auto& warning : result.warnings) {
LOG(WARNING) << "Uninstall warning: " << warning;
}
return cef_installer::FinalizeUninstallLifecycleAfterProgressUi(
lifecycle_context, result,
base::BindOnce(
[](cef_installer::ProgressDialog* dialog,
const cef_installer::Result* result) {
CloseProgressUI(dialog, *result);
},
base::Unretained(dialog.get()), base::Unretained(&result)));
};
// A trusted relaunched child always prepares a fresh controller snapshot
// and runs terminally in-process. It can never launch another child.
if (uninstall_invocation ==
cef_installer::UninstallInvocationState::kRelaunched) {
VLOG(1) << "Running relaunched uninstall from temp directory";
auto preflight = cef_installer::PrepareUninstall(
config, extended, exe_path, uninstall_invocation);
if (!preflight.prepared ||
preflight.decision.execution !=
cef_installer::UninstallExecution::kInProcess) {
LOG(ERROR) << "Relaunched uninstall preflight was rejected";
return cef_installer::FinalizeControlledUninstallFailure(
lifecycle_context,
cef_installer::ControlledUninstallFailure::kPreflightRejected,
preflight.decision.error_code);
}
return run_prepared_uninstall(*preflight.prepared);
}
// The original process resolves the mutation target once before creating
// UI, copying files, acquiring a writer lock, or dispatching a controller.
auto preflight = cef_installer::PrepareUninstall(config, extended, exe_path,
uninstall_invocation);
if (!preflight.prepared || preflight.decision.execution ==
cef_installer::UninstallExecution::kReject) {
LOG(ERROR) << "Uninstall preflight failed";
return preflight.decision.error_code;
}
if (preflight.decision.execution ==
cef_installer::UninstallExecution::kRelaunch) {
VLOG(1) << "Relaunching uninstall from a temporary directory";
const std::wstring relaunch_args =
command_line.GetArgumentsString() + L" /" +
base::UTF8ToWide(cef_installer::kSwitchUninstallRelaunched);
const cef_installer::UninstallRelaunchResult relaunch =
cef_installer::CopySelfToTempAndRelaunch(
exe_path,
config_load.source ==
cef_installer::ConfigLoadSource::kClientResource
? client_dll_module
: nullptr,
relaunch_args, extended.install_path, extended.parent_window);
if (!relaunch.started()) {
LOG(ERROR) << "Required uninstall relaunch failed";
}
return cef_installer::UninstallRelaunchExitCode(relaunch.status);
}
return run_prepared_uninstall(*preflight.prepared);
}
// Handle explicit /cef-update
if (explicit_command == cef_installer::Command::kUpdate) {
VLOG(1) << "Running explicit update command";
auto [dialog, progress] = CreateProgressUI(extended);
cef_installer::Controller controller;
cef_installer::Result result = controller.Run(
cef_installer::Command::kUpdate, config, extended, progress);
CloseProgressUI(dialog.get(), result);
cef_installer::ApplyInstallerResultToStartupState(result, config, extended,
false, startup_state);
if (result.error_code == cef_installer::kExitCodePolicyError) {
LOG(ERROR) << result.error_message;
return result.error_code;
}
if (result.success && !result.libcef_path.empty()) {
VLOG(1) << "Update successful, libcef_path="
<< result.libcef_path.value();
} else {
VLOG(1) << "Update completed: success=" << result.success;
}
// When a client DLL is present, fall through to load it with the resolved
// path (or empty path on failure — the client DLL handles fallback).
// In standalone mode, exit with the installer result code.
if (client_dll_exists) {
return -1;
}
return cef_installer::ResultToExitCode(result);
}
// No explicit command — resolve the installed CEF version.
// When a client DLL is present, always resolve so the path can be passed
// via version_info. In standalone mode (no client DLL), this is auto-install
// — gated by enable_explicit_modes in official builds to prevent a renamed
// binary from silently downloading CEF.
bool should_resolve = client_dll_exists;
if (!should_resolve) {
#if defined(OFFICIAL_BUILD) && defined(NDEBUG)
should_resolve = config.enable_explicit_modes;
#else
should_resolve = true;
#endif
}
if (should_resolve) {
VLOG(1) << "Resolving CEF version (client_dll_exists=" << client_dll_exists
<< ")";
auto [dialog, progress] = CreateProgressUI(extended);
cef_installer::Controller controller;
cef_installer::Result result = controller.Run(
cef_installer::Command::kInstall, config, extended, progress,
cef_installer::ExecutionContext::kAutomaticStartup);
CloseProgressUI(dialog.get(), result);
cef_installer::ApplyInstallerResultToStartupState(result, config, extended,
true, startup_state);
if (result.error_code == cef_installer::kExitCodePolicyError) {
LOG(ERROR) << result.error_message;
return result.error_code;
}
if (result.success && !result.libcef_path.empty()) {
VLOG(1) << "CEF resolved, libcef_path=" << result.libcef_path.value();
} else if (!client_dll_exists) {
// Standalone install failed — exit with error.
LOG(ERROR) << "Auto-install failed: " << result.error_message;
return cef_installer::ResultToExitCode(result);
} else {
// Client DLL present but resolution failed — continue anyway.
// The client DLL can fall back to its own CEF discovery.
VLOG(1) << "CEF resolution failed, client DLL will handle fallback";
}
return -1;
}
VLOG(2) << "No installer action needed";
return -1;
}
void SetInstallerCrashAnnotations(
const cef_installer::InstallerStartupState& state) {
static crashpad::StringAnnotation<16> error("bs-install-error");
static crashpad::StringAnnotation<1024> message("bs-install-msg");
static crashpad::StringAnnotation<64> vmin("bs-install-vmin");
static crashpad::StringAnnotation<64> vmax("bs-install-vmax");
static crashpad::StringAnnotation<4> policy_denied(
"bs-install-policy-denied");
static crashpad::StringAnnotation<4> cancelled("bs-install-cancelled");
const auto values = cef_installer::BuildInstallerCrashAnnotationValues(state);
if (!values.has_failure) {
error.Clear();
message.Clear();
vmin.Clear();
vmax.Clear();
policy_denied.Clear();
cancelled.Clear();
return;
}
error.Set(values.error);
message.Set(values.message);
vmin.Set(values.vmin);
vmax.Set(values.vmax);
if (values.policy_denied.empty()) {
policy_denied.Clear();
} else {
policy_denied.Set(values.policy_denied);
}
if (values.cancelled.empty()) {
cancelled.Clear();
} else {
cancelled.Set(values.cancelled);
}
}
constexpr DWORD kNormalLoad = 0;
template <DWORD LoadFlags>
@@ -297,14 +810,14 @@ int APIENTRY wWinMain(HINSTANCE hInstance,
// those in the Windows thread pool, and others) whose stack size can only be
// controlled via the linker flag.
#if defined(CEF_BUILD_BOOTSTRAP_CONSOLE)
int exit_code = CefRunMainWithPreferredStackSize(main, argc, argv);
int fiber_exit_code = CefRunMainWithPreferredStackSize(main, argc, argv);
#else
int exit_code = CefRunWinMainWithPreferredStackSize(wWinMain, hInstance,
lpCmdLine, nCmdShow);
int fiber_exit_code = CefRunWinMainWithPreferredStackSize(
wWinMain, hInstance, lpCmdLine, nCmdShow);
#endif
if (exit_code >= 0) {
if (fiber_exit_code >= 0) {
// The fiber has completed so return here.
return exit_code;
return fiber_exit_code;
}
#endif
@@ -333,6 +846,18 @@ int APIENTRY wWinMain(HINSTANCE hInstance,
return CEF_RESULT_CODE_BAD_PROCESS_TYPE;
}
// Enable VLOG output based on --v flag. The bootstrap doesn't call
// CommandLine::Init(), so the logging system can't read verbosity from the
// global singleton. Apply it manually.
if (command_line.HasSwitch("v")) {
int verbose_level = 0;
if (base::StringToInt(command_line.GetSwitchValueASCII("v"),
&verbose_level) &&
verbose_level > 0) {
logging::SetMinLogLevel(-verbose_level);
}
}
// Run the crashpad handler now instead of waiting for libcef to load.
constexpr char kCrashpadHandler[] = "crashpad-handler";
if (process_type == kCrashpadHandler) {
@@ -356,6 +881,8 @@ int APIENTRY wWinMain(HINSTANCE hInstance,
std::wstring dll_name;
base::FilePath exe_path;
cef_certificate_util::ThumbprintsInfo exe_thumbprints;
// Owns all installer strings and launch-health data through RunWinMain.
cef_installer::InstallerStartupState installer_state;
if (is_sandboxed) {
// Running as a sandboxed sub-process. May already be locked down, so we
@@ -386,7 +913,15 @@ int APIENTRY wWinMain(HINSTANCE hInstance,
#endif
LOG(FATAL) << "Missing module name";
}
}
// Subprocesses skip certificate validation, installer logic, and the
// untrusted DLL pre-load. The browser process already verified EXE and
// DLL signatures before launching child processes, and propagated the
// validated module name via AppendExtraCommandLineSwitches. Repeating
// three WinVerifyTrust calls per subprocess is redundant and expensive
// (~30-300 ms).
if (!is_subprocess) {
cef_certificate_util::GetClientThumbprints(
exe_path.value(), /*verify_binary=*/true, exe_thumbprints);
@@ -437,10 +972,13 @@ int APIENTRY wWinMain(HINSTANCE hInstance,
// only option that doesn't execute DllMain while still allowing us
// retrieve the path using GetModuleFileName. No execution of the DLL
// should be attempted while loaded in this mode.
const auto result =
const auto untrusted_result =
LoadClientDll<DONT_RESOLVE_DLL_REFERENCES>(dll_name, exe_path);
if (result.module) {
const auto& dll_path = bootstrap_util::GetModulePath(result.module);
const bool client_dll_exists = (untrusted_result.module != nullptr);
if (client_dll_exists) {
const auto& dll_path =
bootstrap_util::GetModulePath(untrusted_result.module);
if (!bootstrap_util::IsModulePathAllowed(dll_path, exe_path)) {
#if DCHECK_IS_ON()
@@ -452,10 +990,32 @@ int APIENTRY wWinMain(HINSTANCE hInstance,
}
CheckDllCodeSigning(dll_path, exe_thumbprints);
}
FreeLibrary(result.module);
} else {
// Save the last error before calling any other function
// Check if installer should run (explicit command or auto-install).
// Pass the untrusted module handle so config can be loaded from client DLL.
int installer_exit = MaybeRunInstaller(
command_line, exe_path, untrusted_result.module, &installer_state);
SetInstallerCrashAnnotations(installer_state);
// Now we can free the untrusted module.
if (untrusted_result.module) {
FreeLibrary(untrusted_result.module);
}
if (installer_exit >= 0) {
// Installer ran and process should exit.
return installer_exit;
}
// In standalone mode (no client DLL), exit after installer completes.
// There is no client DLL to hand off to.
if (!client_dll_exists && !installer_state.libcef_path.empty()) {
return CEF_RESULT_CODE_NORMAL_EXIT;
}
// If client DLL doesn't exist and installer didn't provide a path, fail.
if (!client_dll_exists && installer_state.libcef_path.empty()) {
const DWORD load_error = ::GetLastError();
#if DCHECK_IS_ON()
const auto subst = std::to_array<std::u16string>(
@@ -463,12 +1023,13 @@ int APIENTRY wWinMain(HINSTANCE hInstance,
base::WideToUTF16(cef_util::GetLastErrorAsString())});
ShowError(FormatErrorString(IDS_ERROR_LOAD_FAILED, subst));
#endif
const auto sha1 = CalculateFileSHA1(result.used_path);
const auto sha1 = CalculateFileSHA1(untrusted_result.used_path);
if (!sha1.has_value()) {
LOG(FATAL) << "Failed to read file: " << result.used_path.value()
LOG(FATAL) << "Failed to read file: "
<< untrusted_result.used_path.value()
<< " with error: " << load_error;
} else {
LOG(FATAL) << "Failed to load " << result.used_path.value()
LOG(FATAL) << "Failed to load " << untrusted_result.used_path.value()
<< " with error: " << load_error
<< " SHA1: " << sha1.value();
}
@@ -502,17 +1063,135 @@ int APIENTRY wWinMain(HINSTANCE hInstance,
cef_version_info_t version_info = {};
CEF_POPULATE_VERSION_INFO(&version_info);
// Return immediately without calling FreeLibrary() to avoid an illegal
// access during shutdown. The sandbox broker owns objects created inside
// libcef.dll (SandboxWin::InitBrokerServices) and cleanup is triggered
// via an _onexit handler (SingletonBase::OnExit) called after wWinMain
// exits.
cef_installer::PopulateInstallerVersionInfo(installer_state,
&version_info);
cef_installer::LaunchState sentinel;
// Canonical retention liveness is independent of launch-health mode.
// Publish it before the health sentinel and before client execution.
if (!installer_state.liveness_path.empty() &&
!cef_installer::RefreshLivenessPath(
installer_state.liveness_path, installer_state.appid,
cef_installer::GetCurrentPlatform(),
cef_installer::GetCurrentWallTime())) {
LOG(WARNING) << "Failed to refresh installer liveness record: "
<< installer_state.liveness_path.value();
}
// Write launch health sentinel before RunWinMain (main process only).
if (!installer_state.launch_state_path.empty()) {
sentinel.appid = installer_state.appid;
sentinel.pid = GetCurrentProcessId();
sentinel.pid_start_time = cef_installer::GetCurrentPidStartTime();
sentinel.consecutive_failures =
installer_state.launch_consecutive_failures;
sentinel.running = true;
sentinel.confirmed = false;
sentinel.version = installer_state.launch_version;
sentinel.platform = installer_state.launch_platform;
if (cef_installer::WriteLaunchStatePath(
installer_state.launch_state_path, sentinel)) {
// Publish the active sentinel path so an in-process
// RunInstaller("launch_success") call (HandleLaunchSuccess) can
// resolve and confirm this launch.
cef_installer::SetActiveLaunchStatePath(
installer_state.launch_state_path);
} else {
LOG(ERROR) << "Launch-health sentinel write failed; launching "
"untracked (appid="
<< installer_state.appid
<< ", path=" << installer_state.launch_state_path.value()
<< ", mode="
<< cef_installer::LaunchHealthModeToString(
installer_state.launch_health)
<< ")";
installer_state.launch_state_path.clear();
}
}
// Call RunWinMain and capture the exit code.
// Don't call FreeLibrary() to avoid an illegal access during shutdown.
// The sandbox broker owns objects created inside libcef.dll
// (SandboxWin::InitBrokerServices) and cleanup is triggered via an
// _onexit handler (SingletonBase::OnExit) called after wWinMain exits.
#if defined(CEF_BUILD_BOOTSTRAP_CONSOLE)
return pFunc(argc, argv, &sandbox_info, &version_info);
int exit_code = pFunc(argc, argv, &sandbox_info, &version_info);
#else
return pFunc(hInstance, lpCmdLine, nCmdShow, &sandbox_info,
&version_info);
int exit_code =
pFunc(hInstance, lpCmdLine, nCmdShow, &sandbox_info, &version_info);
#endif
// Client loading/execution has completed. Release the shared-store lease
// before any post-exit pruning so deferred removal can now proceed.
installer_state.version_lease.reset();
// Post-RunWinMain: update launch state based on exit code.
if (!installer_state.launch_state_path.empty()) {
auto ls = cef_installer::ReadLaunchStatePath(
installer_state.launch_state_path);
bool is_ours = ls && ls->pid == sentinel.pid &&
ls->pid_start_time == sentinel.pid_start_time;
if (is_ours) {
// Compose with kLaunchSuccess: if the client already confirmed this
// launch (running==false), the sentinel is already durable — skip
// the write entirely (no-op compose path).
sentinel = *ls;
const bool normal_exit = exit_code == CEF_RESULT_CODE_NORMAL_EXIT;
const bool neutral_exit =
!normal_exit && IsSuccessOrNeutralExitCode(exit_code);
if (cef_installer::ApplyLaunchExit(installer_state.launch_health,
normal_exit, neutral_exit,
&sentinel)) {
if (!cef_installer::WriteLaunchStatePath(
installer_state.launch_state_path, sentinel)) {
LOG(ERROR) << "Failed to persist launch-health exit state (path="
<< installer_state.launch_state_path.value()
<< ", mode="
<< cef_installer::LaunchHealthModeToString(
installer_state.launch_health)
<< ")";
}
}
// Cleanup and pruning run only on a clean exit (code 0). Neutral
// exits (notably "profile in use") can imply a concurrent live
// instance with libcef.dll loaded, or an imminent relaunch — both
// violate the "no in-use file conflicts" premise that justifies
// post-exit pruning. The next clean exit (or a standalone
// /cef-update) still prunes.
if (exit_code == CEF_RESULT_CODE_NORMAL_EXIT) {
for (const auto& path : installer_state.launch_cleanup_paths) {
base::DeleteFile(path);
}
// Post-exit pruning: run with short timeout to avoid blocking exit.
cef_installer::ExtendedConfig prune_ext = installer_state.extended;
prune_ext.lock_timeout_ms = 1;
prune_ext.show_progress_ui = false;
cef_installer::Controller prune_controller;
prune_controller.Run(cef_installer::Command::kPrune,
installer_state.config, prune_ext);
}
}
}
// Off mode has no sentinel, but clean-exit pruning remains active.
if (installer_state.launch_health ==
cef_installer::LaunchHealthMode::kOff &&
exit_code == CEF_RESULT_CODE_NORMAL_EXIT &&
installer_state.configured) {
cef_installer::ExtendedConfig prune_ext = installer_state.extended;
prune_ext.lock_timeout_ms = 1;
prune_ext.show_progress_ui = false;
cef_installer::Controller prune_controller;
prune_controller.Run(cef_installer::Command::kPrune,
installer_state.config, prune_ext);
}
return exit_code;
} else {
// Save the last error before calling any other function
const DWORD proc_error = ::GetLastError();
@@ -0,0 +1,175 @@
# CEF Installer Enterprise Policy
This document is the normative reference for CEF shared-installer enterprise
policy. `README.md` provides an overview and `SECURITY.md` defines trust
boundaries; registry deployment and operational procedures are owned here.
## Registry schema and architecture
Configure machine-wide values under `HKLM\SOFTWARE\Policies\CEF`:
| Value | Type | Default | Meaning |
|-------|------|---------|---------|
| `AllowSharedUserStore` | `REG_DWORD` (`0` or `1`) | `1` | `0` excludes the standard `%LocalAppData%\CEF` store from reads and writes. |
| `CdnUrls` | `REG_MULTI_SZ` | absent | One through three HTTPS base URLs, tried in order, each at most 2048 bytes. |
| `DownloadPath` | `REG_SZ` | absent | Absolute local or UNC root containing the CDN mirror layout. |
| `DisableDownloads` | `REG_DWORD` (`0` or `1`) | `0` | `1` disables every network and mirror fetch. |
`CdnUrls`, `DownloadPath`, and `DisableDownloads=1` are mutually exclusive.
Wrong types, invalid values, unreadable known values, and conflicts return
119/`POLICY_ERROR`; they are never silently ignored. Unknown names are ignored
for forward compatibility.
On 64-bit Windows, policy and `HKLM\SOFTWARE\CEF\InstallLocation` are read from
the 64-bit machine registry view by x86, x64, and ARM64 processes. On 32-bit
Windows the native view is used. There is no redirected-view fallback or
migration. Deployment tools must explicitly target the shared view, for example:
`InstallLocation` is not a value under the policy key and does not mandate an
exclusive policy directory. It supplies the highest-priority standard
directory candidate; normal source-derived role, read, and mutation rules
still apply.
```powershell
reg.exe add "HKLM\SOFTWARE\Policies\CEF" /v AllowSharedUserStore `
/t REG_DWORD /d 0 /f /reg:64
reg.exe add "HKLM\SOFTWARE\Policies\CEF" /v DisableDownloads `
/t REG_DWORD /d 1 /f /reg:64
```
To select ordered CDN URLs instead, use a separate, non-disabled source
configuration:
```powershell
$policy = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Policies\CEF'
New-Item -Path $policy -Force | Out-Null
New-ItemProperty -Path $policy -Name DisableDownloads -PropertyType DWord `
-Value 0 -Force
New-ItemProperty -Path $policy -Name CdnUrls -PropertyType MultiString `
-Value @('https://primary.example/cef/', 'https://backup.example/cef/') `
-Force
```
To switch that configuration to a mirror, first remove `CdnUrls` and keep
downloads enabled:
```powershell
# CdnUrls must be removed before selecting the mutually exclusive mirror.
Remove-ItemProperty -Path $policy -Name CdnUrls
Set-ItemProperty -Path $policy -Name DisableDownloads -Value 0
New-ItemProperty -Path $policy -Name DownloadPath -PropertyType String `
-Value '\\fileserver\cef-mirror' -Force
```
Each operation loads policy once into an immutable snapshot. Concurrent
multi-value registry edits are not transactionally atomic; deployment tooling
should stage changes so it never relies on an intermediate combination being
valid.
## Behavior and precedence
An effective policy download source has highest precedence and replaces rather
than merges with operation and application sources. With no download-source
policy, precedence is the operation-local mirror, the operation `cdn_urls`,
the selected application config's `cdn_urls`, and finally the hardcoded CEF
CDN.
For policy `CdnUrls`, revocation and manifest requests independently try each
origin in order. After selecting a version, the installer requests its hash
sidecar and archive from the same configured origin and tries every origin for
that version before considering version fallback. If the selected archive is
missing from every origin or from the effective mirror, or candidate-specific
validation fails after allowed recovery, the installer may try at most one
next-best compatible version from the same manifest. Cancellation, an outage
across all origins, policy denial, and local write or publication failures are
terminal for the operation. See
[Download Source Precedence and Robustness](README.md#download-source-precedence-and-robustness)
for the complete behavior.
Validated revocation, manifest, and complete archive cache entries are
source-neutral. Resumable archive partials are bound to their configured and
final origins and are discarded when either origin changes.
Installer-owned complete archives and valid partials expire after seven days.
Use `force_check` (`/cef-forcecheck`) when a newly selected source must be
contacted immediately.
`AllowSharedUserStore=0` immediately hides existing shared LocalAppData
versions. HKLM and Program Files provisioning stores remain readable, and a
dedicated explicit-mode executable may mutate an admin store when elevated.
Bundled and unchecked app-shipped CEF remain eligible because the shared
installer is not a security boundary against the application itself.
A distinct `install_path` is application-owned and remains fully functional,
including normal install, update, registration, repair, and pruning. This is
operationally equivalent to app-shipped content. An `install_path` that is
textually or canonically equal to the resolved HKLM, Program Files, or
LocalAppData standard candidate is always configuration error 100, whether or
not policy exists. It never selects that standard store or falls through;
vendors must omit `install_path` to consume a standard location.
`DisableDownloads=1` permits offline selection of provisioned, installed, or
bundled content, but prevents all network and mirror reads. An explicit
`update` still requires its version check and therefore returns policy denial
113. Elevation is not a bypass. Compiled and integrity-valid cached revocations
remain enforced; external `revoked.json` refresh and refresh/cache/backoff
writes are disabled.
Managed applications should ship a bundled fallback. If the effective minimum
version exceeds every provisioned version and no bundled fallback exists,
startup is intentionally broken until an administrator provisions a matching
version or changes policy.
## Provisioning and revocation curation
Use a dedicated installer binary whose trusted resource sets
`enable_explicit_modes: true`; never elevate the application. A scheduled
elevated `/cef-update` can maintain an HKLM or Program Files store while
`AllowSharedUserStore=0`. Do not set `DisableDownloads=1` while that updater is
expected to fetch.
Registrations in an administrator-owned store are durable provisioning pins,
not a census of standard-user consumers. To provision exact version `X`, give
the dedicated bootstrap a unique provisioning `appid` and set `vmin = vmax =
X`, then run `/cef-update`. Give each independent pin its own appid. Run
`/cef-uninstall` with the same config to remove that pin; pruning reclaims only
versions not required by another pin. Revocation remains authoritative over
every pin.
Embed the provisioning config in the dedicated bootstrap for a one-file
artifact. Both an unsigned bootstrap and unsigned `chrome_elf.dll`, or both
binaries signed by the same certificate, are supported. A signed executable
paired with a mismatched `chrome_elf.dll` is rejected. No separate provisioning
command is required.
Standard-user registrations in `%LocalAppData%` never protect an admin-store
version, and admin pins never protect a per-user-store version. Admin-role pins
are not eligible for user-liveness retention. Use distinct custom paths when
applications require incompatible signer policies. This provides operational
isolation, not a security boundary against the owner of a user-writable store.
For an offline fleet, maintain a complete trusted mirror including curated
`revoked.json`:
1. Temporarily clear or set `DisableDownloads` to `0`.
2. Set the mutually exclusive policy `DownloadPath` to the mirror.
3. Run the dedicated updater with `/cef-update /cef-forcecheck` (elevated only
when the target is an admin store).
4. Remove `DownloadPath` and restore `DisableDownloads=1`.
The forced check prevents a fresh source-neutral cache or matching refresh
backoff from suppressing mirror ingestion. The run may also install versions;
there is no revocation-only ingestion command. Command-line
`/cef-download-path` and API `local_download_path` deliberately do not persist
revocations. Never hand-edit integrity-protected `revocation_cache.json`.
## Troubleshooting
| Code | Name | Meaning and remediation |
|------|------|-------------------------|
| 100 | `CONFIG_ERROR` | Application configuration is invalid, including a standard-location `install_path` alias. Fix the vendor configuration. |
| 113 | `POLICY_DENIED` | Valid policy blocked required shared-store mutation or downloading. Review effective vmin and provisioned versions, then contact the administrator. |
| 119 | `POLICY_ERROR` | A known HKLM policy value is unreadable, malformed, wrong-type, or conflicts with another source state. Correct the registry deployment. |
Policy denial is available through structured startup/API results, crash
annotations, normal logs, and standalone output. CEF does not register or
produce Windows Event Log events for these failures.
+58
View File
@@ -0,0 +1,58 @@
# CEF Installer Contributor Guidance
This file is a lightweight guide for changes under this directory. Do not
duplicate installer contracts here; use the following documents as the
authoritative references:
- [README.md](README.md) describes installer behavior, client integration,
configuration, commands, result contracts, and build integration.
- [SECURITY.md](SECURITY.md) defines trust boundaries, security invariants,
required validation, and guards for test-only behavior.
- [TESTING.md](TESTING.md) describes unit, integration, E2E, fuzz, manual, and
coverage testing.
- [ADMIN_POLICY.md](ADMIN_POLICY.md) is the normative reference for enterprise
policy, provisioning, precedence, and policy errors.
When behavior changes, update the owning document and its tests in the same
change. Security-sensitive changes must be checked against `SECURITY.md`,
including its testing-mode guard requirements.
## Code organization
- Production installer code belongs in `cef_installer`. Keep the interface
exposed outside this directory small and task-oriented.
- `cef_installer::internal` is for implementation details and narrowly scoped
test seams. It is not an API boundary. Use it only within installer
implementation files and installer tests.
- `cef_installer::test` is exclusively for test helpers, fixtures, fakes, and
reference implementations. Its files belong in test-only targets. Production
code and production targets must not include, call, or depend on it.
- Do not put production behavior in `*_test_support.*`, and do not put test
infrastructure in production files merely to make it reachable by tests.
Keep production seams minimal; place the fake implementation and assertions
in test-only code.
- A function named `*ForTesting` is still production code when it is compiled
into the installer target. Put such a seam in `internal`, apply the official
build guards required by `SECURITY.md`, and avoid using it from normal
production control flow.
- Do not add environment-variable, command-line, registry, filesystem, timing,
network, or process-launch bypasses ad hoc. Centralize typed test controls,
fail closed in official builds, and document new bypasses in the Testing Mode
Guards table in `SECURITY.md`.
- Prefer testing through public behavior. Expose an `internal` helper only when
direct coverage is materially clearer than exercising the owning public
operation.
Before adding a source or symbol, classify its ownership:
- Production behavior belongs in a normal installer source and in
`cef_installer` or `cef_installer::internal` according to its consumers.
- A seam consulted by production code belongs in `internal`, uses the required
official-build guard, and exposes only the minimum `*ForTesting` control.
- A helper, fake, fixture, or reference implementation used only by tests
belongs in `cef_installer::test`, a clearly named test-support source, and a
test-only target.
Check `BUILD.gn` ownership whenever adding or moving test support. A filename or
preprocessor guard does not make a source test-only: production targets must
not list `*_test_support.*` or depend on `cef_installer::test`.
+284
View File
@@ -0,0 +1,284 @@
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import("//cef/libcef/features/features.gni")
import("//testing/libfuzzer/fuzzer_test.gni")
import("//testing/test.gni")
if (is_win) {
action("cef_installer_config") {
script = "//cef/tools/make_installer_config.py"
inputs = [
"//cef/cef_api_versions.json",
"//cef/tests/cefclient/win/installer_config.json.in",
]
outputs = [
"$root_gen_dir/cef/installer_config.json",
"$root_gen_dir/cef/installer_config.rc",
]
args = [
"--template",
rebase_path("//cef/tests/cefclient/win/installer_config.json.in",
root_build_dir),
"--api-versions",
rebase_path("//cef/cef_api_versions.json", root_build_dir),
"--output-json",
rebase_path(outputs[0], root_build_dir),
"--output-rc",
rebase_path(outputs[1], root_build_dir),
"--resource-name",
"CEF_INSTALLER_CONFIG",
]
if (cef_api_version != "") {
args += [
"--api-version",
cef_api_version,
]
}
}
action("installer_revocation_resource") {
script = "//cef/tools/make_revocation_resource.py"
inputs = [
"//cef/cef_api_versions.json",
"//cef/tools/cefbuilds/revoked.json",
]
outputs = [
"$root_gen_dir/cef/installer/revocation_list.json",
"$root_gen_dir/cef/installer/revocation_list.rc",
]
args = [
"--revoked-json",
rebase_path("//cef/tools/cefbuilds/revoked.json", root_build_dir),
"--api-versions",
rebase_path("//cef/cef_api_versions.json", root_build_dir),
"--output-json",
rebase_path(outputs[0], root_build_dir),
"--output-rc",
rebase_path(outputs[1], root_build_dir),
"--resource-name",
"CEF_REVOCATION_LIST",
]
if (cef_api_version != "") {
args += [
"--api-version",
cef_api_version,
]
}
}
source_set("cef_installer") {
sources = [
"$root_gen_dir/cef/installer/revocation_list.rc",
"installer_archive.cc",
"installer_archive.h",
"installer_bootstrap_helpers.cc",
"installer_bootstrap_helpers.h",
"installer_cdn_manifest.cc",
"installer_cdn_manifest.h",
"installer_config.cc",
"installer_config.h",
"installer_constants.h",
"installer_controller.cc",
"installer_controller.h",
"installer_crc.cc",
"installer_crc.h",
"installer_database.cc",
"installer_database.h",
"installer_download.cc",
"installer_download.h",
"installer_e2e_config.cc",
"installer_e2e_config.h",
"installer_file_integrity.cc",
"installer_file_integrity.h",
"installer_file_ops.cc",
"installer_file_ops.h",
"installer_launch_state.cc",
"installer_launch_state.h",
"installer_lock.cc",
"installer_lock.h",
"installer_lifecycle.cc",
"installer_lifecycle.h",
"installer_logger.cc",
"installer_logger.h",
"installer_parallel_xz.cc",
"installer_parallel_xz.h",
"installer_paths.cc",
"installer_paths.h",
"installer_policy.cc",
"installer_policy.h",
"installer_progress_dialog.cc",
"installer_progress_dialog.h",
"installer_registry.cc",
"installer_registry.h",
"installer_relaunch.cc",
"installer_relaunch.h",
"installer_result_json.cc",
"installer_result_json.h",
"installer_retention.cc",
"installer_retention.h",
"installer_revocation.cc",
"installer_revocation.h",
"installer_scoped_thread_pool.cc",
"installer_scoped_thread_pool.h",
"installer_signature.cc",
"installer_signature.h",
"installer_validation.cc",
"installer_validation.h",
"installer_version.cc",
"installer_version.h",
"installer_version_metadata.cc",
"installer_version_metadata.h",
"installer_version_resolver.cc",
"installer_version_resolver.h",
"installer_xz_index.cc",
"installer_xz_index.h",
]
deps = [
":installer_revocation_resource",
"//base",
"//cef:cef_certificate_util",
"//crypto",
]
libs = [
"advapi32.lib",
"comctl32.lib",
"user32.lib",
]
public_deps = [
"//cef:make_api_versions_header",
"//cef:make_config_header",
"//cef:make_version_header",
"//cef/libcef/features:buildflags",
"//third_party/lzma_sdk",
"//third_party/lzma_sdk:lzma_sdk_xz",
]
configs += [
"//build/config:precompiled_headers",
"//cef/libcef/features:config",
]
}
test("cef_installer_unittests") {
testonly = true
sources = [
"installer_archive_unittest.cc",
"installer_bootstrap_helpers_unittest.cc",
"installer_cdn_manifest_unittest.cc",
"installer_config_unittest.cc",
"installer_controller_unittest.cc",
"installer_database_unittest.cc",
"installer_download_unittest.cc",
"installer_e2e_config_unittest.cc",
"installer_file_integrity_unittest.cc",
"installer_file_ops_unittest.cc",
"installer_integration_test.cc",
"installer_launch_state_unittest.cc",
"installer_lock_unittest.cc",
"installer_lifecycle_test_support.cc",
"installer_lifecycle_test_support.h",
"installer_lifecycle_unittest.cc",
"installer_logger_unittest.cc",
"installer_parallel_xz_unittest.cc",
"installer_paths_unittest.cc",
"installer_policy_unittest.cc",
"installer_progress_dialog_unittest.cc",
"installer_progress_ui_integration_unittest.cc",
"installer_registry_unittest.cc",
"installer_relaunch_unittest.cc",
"installer_resources.rc",
"installer_retention_unittest.cc",
"installer_revocation_unittest.cc",
"installer_signature_unittest.cc",
"installer_test_archive.cc",
"installer_test_archive.h",
"installer_test_catalog.cc",
"installer_test_catalog.h",
"installer_test_helpers.cc",
"installer_test_helpers.h",
"installer_test_resources.h",
"installer_test_resources.rc",
"installer_validation_unittest.cc",
"installer_version_metadata_unittest.cc",
"installer_version_resolver_unittest.cc",
"installer_version_unittest.cc",
"installer_xz_index_unittest.cc",
]
data = [
"testdata/loopback_tls_cert.pem",
"testdata/loopback_tls_key.pem",
"testdata/multi_block_4.tar.xz",
"testdata/multi_block_large.tar.xz",
"testdata/single_block.tar.xz",
"testdata/small_single.tar.xz",
"testdata/test_signing.cer",
"testdata/test_signing.pfx",
"testdata/test_thumbprint.txt",
]
deps = [
":cef_installer",
"e2e/config_test_dlls",
"//base",
"//base/test:run_all_unittests",
"//net:test_support",
"//testing/gtest",
]
configs += [
"//build/config:precompiled_headers",
"//cef/libcef/features:config",
]
}
group("installer_tests") {
testonly = true
deps = [
":cef_installer_unittests",
"e2e",
]
}
fuzzer_test("installer_xz_index_fuzzer") {
sources = [ "installer_xz_index_fuzzer.cc" ]
deps = [
":cef_installer",
"//base",
"//third_party/lzma_sdk",
]
seed_corpus = "testdata"
}
fuzzer_test("installer_tar_fuzzer") {
sources = [ "installer_tar_fuzzer.cc" ]
deps = [
":cef_installer",
"//base",
]
libfuzzer_options = [ "max_len=65536" ]
}
fuzzer_test("installer_xz_block_fuzzer") {
sources = [ "installer_xz_block_fuzzer.cc" ]
deps = [
":cef_installer",
"//base",
"//third_party/lzma_sdk",
"//third_party/lzma_sdk:lzma_sdk_xz",
]
seed_corpus = "testdata"
}
}
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
File diff suppressed because it is too large. Load diff
+509
View File
@@ -0,0 +1,509 @@
# CEF Installer Security Model
This document defines the security invariants, trust boundaries, and defensive
controls for the CEF installer library. It is the authoritative reference for
developers modifying this code.
---
## How to Use This Document
### When writing implementation plans
Reference this document in the plan and include a security section that maps
planned changes to affected invariants. For each new code path, check every
"If you change" callout in the relevant invariant sections and add
corresponding exit criteria. Example from the parallel XZ extraction plan:
> **Security (S1):** `ExtractTarFromBuffer()` enforces `kMaxFileSize`,
> `kMaxTotalExtractionSize`, `kMaxEntryCount`, and skips symlinks
> *(from Invariant 3: Archive Extraction)*
### When reviewing plans or code
Use the invariant tables as a checklist. For each file touched by the change,
find the matching invariant section and verify every row still holds. Pay
special attention to:
- New data paths that bypass existing validation
- New testing bypasses that need `OFFICIAL_BUILD && NDEBUG` guards
- New check-then-use patterns that should be added to the TOCTOU table
### When updating this document
Update invariant tables when:
- A new security control is added (new row)
- An existing control moves to a different file or function (update location)
- A new TOCTOU race or testing bypass is introduced (add to relevant table)
Do NOT add review progress, findings, or task-specific notes here. Those
belong in review plans or issue trackers.
---
## Trust Boundaries
| Boundary | Trusted | Untrusted |
|----------|---------|-----------|
| CDN HTTPS | Installer process | CDN server responses (manifests, archives, revocation list) |
| Disk (admin dirs) | Administrator-owned ACL boundary | Standard users and lower-integrity processes |
| Disk (`%LocalAppData%`/user custom dirs) | User-integrity applications share the user's authority | Network input and accidental corruption; not arbitrary native code running as that user |
| Application CDN selection | Client and bootstrap resources execute at the owning user's integrity | Not an administrator boundary; another native process running as that user has equivalent authority |
| IPC (WM_COPYDATA) | Installer mutation/result state and nonce-bound relaunch context | Parent window and all messages/return values; public lifecycle IDs provide correlation, not authentication |
| Config sources | Client DLL and bootstrap embedded resources (authoritative when selected) | Command-line args; `RunInstaller` JSON is caller-controlled operation input |
| Archive contents | N/A (always untrusted) | tar.xz entries: filenames, sizes, types, compressed data |
| Revocation cache | Compiled baseline (immutable RT_RCDATA) | Disk-cached CDN delta (writable by install-dir owner) |
---
## Assumptions
- HTTPS/TLS termination is sound (system cert store is trusted).
- The code signing certificate private key is not compromised.
- The compiled-in revocation baseline cannot be tampered with at runtime.
- Chromium `base::` primitives (file I/O, crypto, threading) are sound.
---
## Security Invariants
### 1. Signature Verification
Every downloaded CEF distribution MUST be verified against a pinned
certificate thumbprint before publication. The default user-writable store is
not an ongoing authenticity boundary against arbitrary native code running as
that user. Client-side runtime signer checks are defense in depth and do not
reauthenticate every catalog-covered dependency/resource.
Bundled CEF is a different trust path. `CheckBundledCef()` requires
`catalog.cat` to exist as a regular non-reparse file, but that is only a layout
check; startup does not verify its signature or catalog members. Bundled
integrity rests on the application's signed package.
| Invariant | Enforced by | File |
|-----------|-------------|------|
| All installed files are verified against a signed catalog (`catalog.cat`) | `VerifyWithCatalog()` | `installer_signature.cc` |
| Catalog signature is verified BEFORE file hashes are checked | `VerifyWithCatalog()` call order | `installer_signature.cc` |
| Certificate thumbprint is always pinned (never empty) in production paths | `kCefCertificateThumbprint` default, traced from `DownloadAndInstall()` | `installer_constants.h`, `installer_controller.cc` |
| Bundled `catalog.cat` presence is a non-reparse layout check, not runtime authentication | `CheckBundledCef()` | `installer_controller.cc` |
| `SetSignatureTestingMode()` is a no-op in official builds | `#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))` guard | `installer_signature.cc` |
| Authenticode revocation checking is disabled (no network dependency) | `WTD_REVOKE_NONE` flag | `installer_signature.cc` |
| `GetSignatureThumbprint()` only returns thumbprints from valid signatures | Return-empty-on-failure pattern | `installer_signature.cc` |
A trusted `RunInstaller` caller may replace the default CEF thumbprint with
another non-empty expected thumbprint. This still requires a Windows-valid
catalog signature matching that certificate; it does not permit unsigned
publication in official builds. Alternative-signer applications should use
distinct `install_path` namespaces to prevent compatibility collisions. This
separation is not a boundary against the user who owns a user-writable store.
The cefclient reference additionally pins CDN-installed `libcef.dll` at load
time as defense in depth, but does not reauthenticate every catalog member.
**If you change:** A new downloaded-install publication path MUST call
`VerifyWithCatalog()` with a non-empty `expected_thumbprint` before
publication. A new bundled/load path must document and test its source-
appropriate trust behavior instead of implying ongoing catalog authentication.
### 2. Download Security
All network downloads MUST use HTTPS and enforce size limits.
| Invariant | Enforced by | File |
|-----------|-------------|------|
| Only HTTPS URLs accepted in production | `IsValidDownloadUrl()` | `installer_download.cc` |
| `allow_http_for_testing` compiled out in official builds | `#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))` | `installer_download.cc` |
| Downloads capped at `kDefaultMaxDownloadSize` (500 MB); revocation list capped at 1 MB | `DownloadOptions::max_download_size` check after download | `installer_download.cc`, `installer_controller.cc` |
| File deleted on size-limit excess | Post-download check in `DownloadFile()` | `installer_download.cc` |
| SHA256 hashing uses streaming (no full-file memory load) | `ComputeFileSha256()` via `crypto::SecureHash` | `installer_download.cc` |
| Hash mismatch causes file deletion | Error path in `DownloadFile()` | `installer_download.cc` |
| Generic downloads clean temporary files; resumable archives retain only safe origin-bound partials after interruption/cancellation and prune them after seven days | `DownloadFile()`, `PruneCacheDirectory()` | `installer_download.cc` |
| HKLM policy URL lists require HTTPS and forbid userinfo, query, and fragment components | `ValidateEnterprisePolicySnapshot()` | `installer_policy.cc` |
| Policy mirrors retain mandatory hash/catalog/signature validation | Effective source selects transport only | `installer_policy.cc`, `installer_controller.cc` |
| Application `cdn_urls` are one-to-three bounded HTTPS prefixes without userinfo/query/fragment | Shared validation before source resolution | `installer_config.cc`, `installer_policy.cc`, `installer_controller.cc` |
| Resumable archive state is bound to configured and post-redirect final-origin digests | Deterministic partial naming and pre-body origin comparison | `installer_download.cc` |
| A partial appends only after a strict single-range `206`; `200` restarts from zero | Range state machine and `Content-Range` parser | `installer_download.cc` |
| Complete cache publication requires final length and SHA256/SHA1 validation | Hash-before-move promotion | `installer_download.cc` |
HKLM enterprise policy is administrator authority over shared-installer
behavior. Policy URL and mirror sources override application/operation
transport inputs, but do not weaken post-download verification. A policy
mirror may persist revocation deltas so read-only managed users consume the
administrator-curated view; command-line and API local paths cannot. See
[ADMIN_POLICY.md](ADMIN_POLICY.md) for registry and operational procedures.
`DisableDownloads` accepts the availability tradeoff that revocation freshness
becomes administrator-curated. It stops external refreshes but never disables
compiled or integrity-valid cached revocation enforcement. Policy does not
restrict bundled or unchecked CEF shipped by the application itself; the
shared installer is not a security boundary against same-user application
code.
Application source configuration in a client DLL or bootstrap resource
likewise operates at user integrity. HTTPS,
transport hashes, catalog/signature verification, the compiled revocation
baseline, the vmin clamp, platform checks, and ABI matching protect source
authenticity, corruption detection, and compatibility. They do not create a
security boundary against arbitrary native code already running as that user.
A stale or misconfigured application/policy mirror can reduce availability,
serve an older still-valid build, omit a newer revocation delta, or consume
cache space with failed downloads. Per-download limits, the additive compiled
revocation baseline, mandatory signature/hash checks, and seven-day complete/
partial archive pruning bound those effects; there is intentionally no
aggregate cache quota.
Completed archives and installed distributions do not retain origin
provenance because their hashes and signed catalogs establish content
identity. Transient partials are different: their filenames carry only bounded
digests of the normalized configured and post-redirect final origins. A change
to either identity discards the partial before response bytes can be appended,
so one archive body is never assembled across origins. Raw URLs, paths,
queries, fragments, and credentials are not stored in partial names or
metadata.
**If you change:** Adding a new download path? It MUST go through
`DownloadFile()` or `DownloadToString()` — never call WinHTTP functions
directly.
### 3. Archive Extraction
All archive extraction paths MUST enforce resource limits and path safety.
These limits exist in `installer_archive.cc` and MUST be replicated in any
new extraction path (e.g., parallel extraction, buffer-based extraction).
| Limit | Constant | Value | Purpose |
|-------|----------|-------|---------|
| Max decompressed buffer | `kMaxDecompressedSize` | 512 MB | Prevents decompression bomb in streaming buffer |
| Max file size | `kMaxFileSize` | 4 GB | Per-file extraction limit |
| Max total extraction | `kMaxTotalExtractionSize` | 10 GB | Aggregate across all entries |
| Max path length | `kMaxPathLength` | 4096 | Prevents excessive path construction |
| Max entry count | `kMaxEntryCount` | 100,000 | Prevents entry-count DoS |
| Invariant | Enforced by | File |
|-----------|-------------|------|
| Every entry path passes through `IsPathSafe()` | `TarReader::IsPathSafe()` (both string and FilePath overloads) | `installer_archive.cc` |
| `IsPathSafe()` rejects: `../`, absolute paths, UNC paths, Windows reserved names, null bytes, oversized paths | Explicit checks in `IsPathSafe()` | `installer_archive.cc` |
| PAX path overrides pass through `IsPathSafe()` | Check after `ParsePaxPath()` | `installer_archive.cc` |
| GNU long name paths pass through `IsPathSafe()` | Check after long-name parsing | `installer_archive.cc` |
| Symlinks are skipped (never extracted) | Type check in extraction loop | `installer_archive.cc` |
| Hard links, device nodes, FIFOs are rejected | Type check → `kOther` | `installer_archive.cc` |
| Size arithmetic uses `base::CheckedNumeric` | Checked accumulation in extraction loop | `installer_archive.cc` |
| Tar header checksum is validated | `ValidateChecksum()` | `installer_archive.cc` |
**If you change:** Adding a new extraction path (e.g., `ExtractTarFromBuffer`,
parallel block extraction)? It MUST enforce ALL of the above limits. Copy the
security checks from the existing streaming extraction loop, and add or extend
a fuzz target as described in [Fuzz Testing](TESTING.md#fuzz-testing).
### 4. File System & Path Security
All file operations on install directories MUST check for reparse points
(symlinks/junctions) and validate paths.
| Invariant | Enforced by | File |
|-----------|-------------|------|
| Readable dirs never include lower-priority locations than the writable dir | `ResolveInstallDirectories()` stops at first writable | `installer_paths.cc` |
| Install/uninstall source dirs checked for reparse points | `IsDirectorySafe()` → `IsReparsePoint()` | `installer_file_ops.cc` |
| All discovered install paths checked for reparse points | `IsReadableDirectory()` → `IsReparsePoint()` | `installer_paths.cc` |
| `Versions/` parent directory checked for reparse points | `IsReparsePoint(versions_dir)` before enumerating or installing | `installer_file_ops.cc`, `installer_paths.cc`, `installer_version_metadata.cc` |
| Version directories checked during scanning | `ScanInstalledVersions()` reparse check | `installer_paths.cc` |
| Pre-operation reparse point cleanup (staging, cache, archive paths) | `VerifySafeFilePath()` / `VerifySafeDirectoryPath()` — removes reparse point then optionally deletes | `installer_file_ops.cc`, `installer_controller.cc` |
| Cache directory checked before read/write/enumerate | `IsReparsePoint()` in `DownloadWithCache()`, `PruneCacheDirectory()` | `installer_download.cc` |
| Cache files checked before read (via `IsCacheValid()`) and before write | `IsReparsePoint()` in `IsCacheValid()`, `DownloadWithCache()` | `installer_download.cc` |
| Cache, revocation, and database files have CRC32 integrity footer | `WriteFileWithIntegrity()` / `ReadFileWithIntegrity()` — doom on mismatch | `installer_file_integrity.cc` |
| Database uses atomic write (temp file + rename) | `Database::Save()` via `base::ReplaceFile()` | `installer_database.cc` |
| Database corruption suspends pruning for 7-day grace period | `SuspendPruning()` / `CanPrune()` with persisted `pruning_suspended_until` timestamp | `installer_database.cc`, `installer_controller.cc` |
| Revocation cache file checked before read and write | `IsReparsePoint()` on dir and file in both `WriteRevocationCache()` and `LoadRevocationCache()` | `installer_revocation.cc` |
| Temp directory checked after creation (relaunch) | `IsReparsePoint()` after `CreateDirectory()` | `installer_relaunch.cc` |
| Registry paths come from HKLM only (not HKCU) | `HKEY_LOCAL_MACHINE` in `ResolveRegistryPath()` | `installer_paths.cc` |
| Registry paths validated as absolute, no parent refs | `IsAbsolute()`, `!ReferencesParent()` | `installer_paths.cc` |
| Custom `install_path` is exclusive (fail hard, no fallback) | Early return in `ResolveInstallDirectories()` | `installer_paths.cc` |
| Custom `install_path` remains readable when safe but non-writable; query does not create or probe it | operation-aware custom resolution | `installer_paths.cc` |
| Directory roles come from candidate source, not ACLs/path prefixes | role-tagged candidate construction and stable deduplication | `installer_paths.cc` |
| Official automatic startup cannot mutate admin-default roles without the trusted bootstrap gate | `IsAdminMutationAllowed()` before candidate write probing | `installer_paths.cc`, `installer_controller.cc` |
| All returned libcef.dll paths validated for reparse points before use | `MakeValidatedLibcefResult()` → `IsPathSafeForLoading()` | `installer_controller.cc`, `installer_file_ops.cc` |
| Launch state read/write/scan check `.launch/` dir for reparse points | `IsReparsePoint()` guard in `ReadLaunchStatePath()`, `WriteLaunchStatePath()`, and `ScanLaunchStates()`; writer-locked controller paths repair unsafe directories | `installer_launch_state.cc`, `installer_controller.cc` |
| Launch records require a valid CRC footer and bounded current-schema JSON; health `last_update` and liveness `last_launch` must be nonzero | `ReadJsonWithIntegrity()`, `ParseLaunchStateDict()`, `ParseLivenessDict()` | `installer_launch_state.cc` |
| Launch-record GC trusts only CRC-verified content time, ignores filesystem timestamps, preserves future timestamps and unavailable time, and expires at the exact 90-day boundary | `ClassifyLaunchStateGcAge()`, `PruneUnusedVersions()` | `installer_launch_state.cc`, `installer_controller.cc` |
| GC observes one bounded no-follow publication with its resolved parent and conditionally deletes only the identical content at that location; invalid repair uses exact raw bytes, an opened-object oversized check, or same-object reparse removal | `ReadLaunchRecordSnapshot()`, `DeleteFileWithIntegrityIfMatching()`, `DeleteFileRawIfMatching()`, `DeleteFileIfOversized()`, `DeleteFileIfReparsePoint()` | `installer_launch_state.cc`, `installer_controller.cc`, `installer_file_integrity.cc` |
| Read-only launch-state paths preserve corrupt, footerless, old-schema, noncanonical, and indeterminate files; writer-locked prune conditionally repairs only conclusive observations | preserve-on-mismatch readers and `PruneUnusedVersions()` repair snapshots | `installer_launch_state.cc`, `installer_controller.cc` |
| Orphaned and below-vmin launch records never protect executable versions, including while age-retained or when time is future/unavailable | GC eligibility before confirmed protection in `PruneUnusedVersions()` | `installer_controller.cc` |
| Empty install roots never produce relative `.launch` paths | Launch-health evaluation returns early and launch-state result paths require a non-empty install directory | `installer_launch_state.cc`, `installer_controller.cc` |
| Atomic rename via `MoveFileExW` with `MOVEFILE_WRITE_THROUGH` | `InstallVersion()` | `installer_file_ops.cc` |
| Staging under install dir guarantees same-volume atomic rename | `<install_dir>/.staging/` in `DownloadAndInstall()` | `installer_controller.cc` |
| Trash directory uses cryptographic random suffix | `base::RandUint64()` in `GenerateTrashPath()` | `installer_file_ops.cc` |
| Once verified staging exists, every safe regular-file or directory publication target is moved whole to reparse-safe `.trash/` without reading its contents; verified staging is then renamed into place, and reclamation deletes nested reparse points without traversing them | `InstallVersion()` | `installer_file_ops.cc` |
| Mandatory target replacement is limited to verified install/update publication under the writer lock; missing/damaged-index recovery, query/read-only selection, prune, and retention scans remain unchanged | Controller call graph | `installer_controller.cc` |
Launch health is heuristic local compatibility data, not authentication,
security policy, or fleet-wide revocation. It is disabled by default and keyed
by appid, platform, and version. Opted-in classification uses a boot boundary
and PID creation time to reduce stale-process attribution, but task termination,
OOM, and logoff remain ambiguous. Integrity writes use same-directory atomic
replacement; CRC detects corruption but does not authenticate content.
Launch-record age is availability-oriented cleanup metadata, not a trust
boundary: writers own health `last_update`, zero is invalid, future values and
an unavailable clock preserve state, and filesystem times are never evidence.
**Priority search and privilege separation:** `ResolveInstallDirectories()`
walks candidates in priority order (registry → ProgramFiles → LocalAppData),
collecting readable directories and stopping at the first permitted writable
one. Roles are source-derived. Elevated processes never continue to
LocalAppData. In official automatic startup, a closed trusted explicit-mode
gate makes HKLM and ProgramFiles read-only even if their ACLs permit writes.
Custom paths are deployment-owned and require ACLs appropriate to the
application's execution level.
**If you change:** Adding a new path discovery source or file operation? For
read-only checks (enumeration, validation), use `IsReparsePoint()` or
`IsReadableDirectory()`. For paths that need reparse-point removal before use
(e.g., staging or cache directories the installer is about to create or write
to), use `VerifySafeFilePath()` or `VerifySafeDirectoryPath()`. Be aware of
TOCTOU — minimize the window between the check and the operation.
### 5. Config & Input Parsing
All parsed data from untrusted sources MUST be length-bounded and validated.
| Invariant | Enforced by | File |
|-----------|-------------|------|
| Client resources win when valid; only absence permits bootstrap-resource fallback; standalone mode reads only the bootstrap resource | `TryLoadInstallerConfig()` tri-state result and source provenance | `installer_bootstrap_helpers.cc` |
| Malformed or invalid selected resources fail with config error 100 and never fall through | `TryLoadInstallerConfig()` tri-state result | `installer_bootstrap_helpers.cc` |
| `install_path` is accepted from the client DLL resource only, resolved relative to that DLL, and ignored in bootstrap resources | parse option plus `ResolveClientInstallPath()` | `installer_config.cc`, `installer_bootstrap_helpers.cc` |
| Unrecognized application-config keys warn individually but remain non-fatal for forward compatibility | recognized-key audit in `ParseConfigFromJson()` | `installer_config.cc` |
| Application `cdn_urls` accepted from the selected client/bootstrap resource; operation `cdn_urls` accepted from `RunInstaller` JSON | Shared strict one-to-three URL-list parser; selected application lists never merge | `installer_config.cc`, `installer_controller.cc`, `installer_policy.cc` |
| HTTPS enforced for every parsed `cdn_urls` entry | `ValidateAndNormalizeCdnUrls()` before source resolution and `IsValidDownloadUrl()` before requests | `installer_policy.cc`, `installer_download.cc` |
| CDN manifest filenames validated | `IsValidArchiveFilename()` before URL construction | `installer_cdn_manifest.cc` |
| JSON string fields bounded by `kMax*Length` constants in CDN manifest, database, and metadata parsers | `ParseBuildEntry()`, `Database::Load()`, `ReadVersionMetadata()` | `installer_cdn_manifest.cc`, `installer_database.cc`, `installer_version_metadata.cc` |
| Log messages sanitized against injection | `SanitizeForLog()` escapes control characters | `installer_logger.cc` |
| Parent HWND treated as untrusted (no ownership, only positioning) | `GetWindowRect()` only, with `IsWindow()` check | `installer_progress_dialog.cc` |
| `parent_window` parses a decimal string or exactly represented numeric value with `uintptr_t` bounds; truncation and rounding are rejected | `ParseExtendedConfigFromJson()`, `ParseExtendedConfigFromCommandLine()` | `installer_controller.cc`, `installer_bootstrap_helpers.cc` |
| Official builds read `enable_explicit_modes` only from the bootstrap's embedded resource; a missing or false gate blocks explicit commands and standalone auto-install | trusted bootstrap provenance and explicit-mode checks | `installer_bootstrap_helpers.cc`, `bootstrap_win.cc` |
| Structured installer error messages are diagnostic data only and are never interpreted as commands or paths | Startup result and JSON serialization only | `installer_bootstrap_helpers.cc`, `installer_controller.cc` |
| Known HKLM policy values are exact-type validated in one immutable operation snapshot; malformed/conflicting policy returns 119 | `LoadEnterprisePolicy()`, `ValidateEnterprisePolicySnapshot()` | `installer_policy.cc`, `installer_registry.cc` |
| `WM_COPYDATA` progress and lifecycle messages contain no credentials, nonce, or filesystem paths | Bounded progress fields and normalized path-free lifecycle results | `installer_controller.cc`, `installer_lifecycle.cc` |
See [Config Security](README.md#config-security) for the integrator-facing
configuration source priority and fallback behavior.
The explicit-mode gate prevents a trusted, code-signed bootstrap from being
repurposed through renaming or copying. Signature verification prevents
arbitrary downloaded code, but without this gate the bootstrap could still be
abused for network access, disk consumption, or installer-state poisoning. See
[Standalone Mode Security](README.md#standalone-mode-security) for the
integrator-facing behavior matrix.
The parent HWND may belong to a lower-integrity process while the installer is
elevated. The installer therefore never establishes window ownership, changes
the parent window's state, or grants it control over another process. It uses
the HWND only for positioning and bounded progress/cancellation messages. See
[Parent Window Handle](README.md#parent-window-handle-cross-process-security)
for the integration contract.
**Field length limits** (from `installer_constants.h`):
| Constant | Limit | Used for |
|----------|-------|----------|
| `kMaxVersionLength` | 64 | Version strings |
| `kMaxUuidLength` | 256 | Application UUIDs |
| `kMaxAbiHashLength` | 256 | ABI compatibility hashes |
| `kMaxFilenameLength` | 256 | Archive filenames |
| `kMaxSha1Length` | 40 | SHA1 hex strings |
| `kMaxSha256Length` | 64 | SHA256 hex strings |
| `kMaxTimestampLength` | 64 | ISO 8601 timestamps |
| `kMaxVersionFullLength` | 256 | Full version strings |
| `kMaxPlatformLength` | 32 | Platform strings |
| `kMaxReasonLength` | 1024 | Revocation reason strings |
| `kMaxRevocationDownloadSize` | 1 MB | CDN revocation list download cap |
| `kMaxRevocationCacheFileSize` | 2 MB | On-disk revocation cache file size cap |
| `kMaxRevocationEntryCount` | 10,000 | Revocation list entry count cap (prevents CPU exhaustion during parse/merge) |
**If you change:** Adding a new JSON parser or data source? It MUST enforce
length limits from `installer_constants.h` on all string fields. If you need
a new limit, add it to that file with a comment explaining the rationale.
### 6. Version Revocation
Revoked versions MUST be excluded from installation and use.
| Invariant | Enforced by | File |
|-----------|-------------|------|
| Compiled baseline loaded from immutable `RT_RCDATA` resource | `LoadCompiledRevocationList()` | `installer_revocation.cc` |
| `MergeRevocationLists()` is additive-only (CDN cannot shrink baseline) | Union merge logic | `installer_revocation.cc` |
| Revoked installed versions excluded from selection | `FindBestVersion()` filters against revocation list | `installer_version_resolver.cc` |
| Revoked CDN versions blocked from download | `IsVersionRevoked()` check before download | `installer_controller.cc` |
| Revoked bundled versions demoted | Priority adjustment | `installer_controller.cc` |
| Effective revocations merge the compiled baseline with valid caches in every readable directory | `LoadEffectiveRevocationList()` seed passed through selection and prune | `installer_revocation.cc`, `installer_controller.cc` |
| Revocation is authoritative over registration and confirmed prune protection | `GetRequiredVersionSet()`, `GetPrunableVersions()` | `installer_version_resolver.cc` |
**Accepted deviation:** The CDN revocation list is not cryptographically
signed. Residual risk is mitigated by the compiled baseline (cannot be
removed) and by the fact that adding fake entries only causes DoS (revokes
legitimate versions), not code execution.
**If you change:** Adding a new version source (e.g., local cache, peer
discovery)? It MUST be filtered against the merged revocation list before use.
### 7. Concurrency & Locking
Only one installer instance may modify a given install directory at a time.
| Invariant | Enforced by | File |
|-----------|-------------|------|
| Global named mutex with `Global\` prefix | `SingletonLock::Acquire()` | `installer_lock.cc` |
| Lock scoped per install directory (hash-based name) | Name derivation in `SingletonLock` | `installer_lock.cc` |
| Kernel abandonment grants exactly one waiter ownership; same-thread recursion is rejected and release asserts owner thread | `WAIT_ABANDONED` handling and owner tracking | `installer_lock.cc` |
| Historical semaphore/new mutex object-type collision fails closed | `CreateMutexW()` failure on existing semaphore name | `installer_lock.cc` |
| RAII release (destructor releases mutex) | `~SingletonLock()` | `installer_lock.cc` |
| Authoritative database writes occur under the mutex; startup registration is zero-timeout best effort | `Controller::Run()`, `TryRegisterStartup()` | `installer_controller.cc` |
| Registration, index publication, reconciliation, and prune ownership are scoped to one selected writable directory; cross-store registrations are not aggregated | controller passes only `install_dir` to authoritative operations | `installer_controller.cc` |
| Query and automatic-startup offline selection do not acquire the writer mutex | pre-lock dispatch and `ResolveStartupOffline()` | `installer_controller.cc` |
| Automatic-startup emergency scanning is limited to confirmed missing/corrupt indexes; non-dooming reads fail closed on generic I/O errors and valid indexes remain authoritative | per-root index status plus bounded scan in `ResolveStartupOffline()` | `installer_controller.cc`, `installer_version_metadata.cc` |
| Installed startup result holds a no-delete `libcef.dll` lease through client execution | `AcquireVersionLease()`, `InstallerStartupState` | `installer_file_ops.cc`, `bootstrap_win.cc` |
| Checked add publication is directory -> expanded index -> registration | `DownloadAndInstall()`, `PublishRegistration()` | `installer_controller.cc` |
| Checked removal publication is registration -> reduced index -> whole-directory trash move | `UpdateDatabase()`, `PruneUnusedVersions()` | `installer_controller.cc` |
| Valid indexes are authoritative during mutation recovery; missing/corrupt indexes are conservatively rebuilt before orphan classification | `ReconcileInstallState()` | `installer_controller.cc` |
| Query/read-only selection never invokes reconciliation, repair, prune, network, or cache mutation | command dispatch and `LoadQueryRevocations()` | `installer_controller.cc` |
| Only an integrity-valid, parseable fresh revocation cache means zero HTTP; stale/invalid launch refresh uses a single-attempt asynchronous request canceled at an absolute deadline and integrity-protected failure backoff | cache freshness/backoff gate and asynchronous request cancellation | `installer_revocation.cc`, `installer_download.cc`, `installer_controller.cc` |
**If you change:** Adding a new operation that modifies the install directory
or database? It MUST run while `SingletonLock` is held.
### 8. Process Launch (Uninstall Relaunch)
Self-copy-and-relaunch for uninstall MUST not introduce privilege escalation
or path injection vectors. Routing MUST select a mutation-permitted target
before authorizing mutation and MUST fail closed when physical containment
cannot be established. Once relaunch is required, failure to prepare or start
the child MUST NOT fall back to direct mutation.
| Invariant | Enforced by | File |
|-----------|-------------|------|
| Original uninstall resolves enterprise policy and one mutation-permitted writable target before routing; no writable target cannot trigger relaunch | `PrepareUninstall()`, `ResolveInstallDirectories()` | `installer_bootstrap_helpers.cc`, `installer_paths.cc`, `installer_policy.cc` |
| Bootstrap CLI direct dispatch requires a prepared snapshot bound to uninstall, configuration, install path, and the `kInProcess` decision; `RunInstaller("uninstall")` resolves its own state through `Controller::Run()` | `MaybeRunInstaller()`, `Controller::RunPreparedUninstall()` validation | `bootstrap_win.cc`, `installer_controller.cc` |
| Physical containment is tri-state over handle-resolved identities; an indeterminate ordinary-uninstall result returns 105 before mutation or relaunch | `GetPhysicalPathContainment()`, `DecideUninstallExecution()` | `installer_paths.cc`, `installer_bootstrap_helpers.cc`, `bootstrap_win.cc` |
| The internal relaunch marker and state are accepted only as a complete pair for explicit uninstall; the child executable must be physically contained by the installer temp directory and state must pass reparse, size, JSON, nonce, and absolute-path validation | `ResolveUninstallInvocationContext()`, `IsRunningFromTempDirectory()`, `GetPhysicalPathContainment()`, `ReadUninstallRelaunchState()` | `installer_bootstrap_helpers.cc`, `installer_paths.cc`, `installer_relaunch.cc` |
| Temp directory uses 64-bit cryptographic random suffix | `base::RandUint64()` | `installer_relaunch.cc` |
| Temp directory checked for reparse points after creation | `IsReparsePoint()` post-`CreateDirectory()` | `installer_relaunch.cc` |
| `CreateProcessW` uses full path (no PATH search) | Explicit path construction | `installer_relaunch.cc` |
| Exe path is quoted in command line | Quoting in command-line construction | `installer_relaunch.cc` |
| Relaunched process inherits same privilege level | No elevation request | `installer_relaunch.cc` |
| Failure to prepare or start a required relaunch returns 105 and never falls back to direct mutation | terminal relaunch branch in `MaybeRunInstaller()` | `bootstrap_win.cc` |
| Optional lifecycle state binds one public operation ID and canonical decimal parent HWND to the private nonce state; partial, malformed, overflowed, or command-mismatched values fail closed | `WriteUninstallRelaunchState()`, `ReadUninstallRelaunchState()` | `installer_relaunch.cc` |
| Lifecycle handoff occurs only after successful child creation; terminal results occur only after valid state acceptance and controlled child finalization | `CopySelfToTempAndRelaunch()`, `FinalizeUninstallLifecycle()` | `installer_relaunch.cc`, `bootstrap_win.cc`, `installer_lifecycle.cc` |
| Lifecycle sends are one-shot and bounded to 500 ms; receiver results cannot cancel or alter commit/exit status | `SendInstallerLifecycleMessage()` | `installer_lifecycle.cc` |
| Lifecycle payloads are valid UTF-8 with one NUL, 32 KiB total, bounded diagnostics, and path-free normalized messages | serializers/reference parsers and result producers | `installer_lifecycle.cc`, `installer_lifecycle_test_support.cc`, `installer_result_json.cc`, `installer_controller.cc` |
| Child terminal failures accept only explicit stable installer error codes; exit 109 and arbitrary `UNKNOWN_ERROR` integers are rejected | lifecycle serializer and reference parsers | `installer_lifecycle.cc`, `installer_lifecycle_test_support.cc`, `e2e/lifecycle_receiver.py` |
| Reference correlation state is capped at 256 operations, expires after 30 idle minutes, prioritizes orphan-terminal eviction, and exposes consume/remove operations | `InstallerLifecycleCorrelator`, `Correlator` | `installer_lifecycle_test_support.cc`, `e2e/lifecycle_receiver.py` |
The private relaunch nonce authorizes trusted-state consumption. The public
128-bit operation ID only correlates a handoff with a terminal result; it does
not authenticate the sender or receiver. Another process at the same user and
integrity level may spoof window messages, and HWND destruction/reuse can
redirect a later best-effort send. No receiver response carries a command or
can change installer work.
Windows UIPI may block delivery from a lower-integrity child to a
higher-integrity receiver. Such a receiver controls any opt-in by calling
`ChangeWindowMessageFilterEx(WM_COPYDATA, MSGFLT_ALLOW)` on its own window. The
installer does not weaken message filters. A blocked, timed-out, destroyed, or
hung destination is an indeterminate transport outcome, never a reason to
retry or roll back the uninstall.
---
## Registration Retention Availability Tradeoff
Registration retention is explicit maintenance, not an uninstall detector or
a security boundary. It uses durable registration identity and canonical,
integrity-valid launch evidence; it never infers ownership or abandonment from
executable paths, ACLs, writability, or elevation. Missing, malformed,
noncanonical, mismatched, zero, or future-dated evidence fails closed as
unknown/protected.
Only source-derived per-user-default and custom `install_path` stores
are eligible. HKLM and Program Files roles are provisioning-owned and
ineligible even when their paths or permissions look user-writable. Apply
recomputes under the writer lock and performs one authoritative final evidence
collection. Each registration's direct canonical-liveness observation, rather
than completion of the sequential store scan, is its launch-intent cutoff.
Apply then offers a final cancellation checkpoint and enters the `committing`
state. Logical database/index publication cannot be cancelled after that
transition, but the built-in UI and parent-window protocol continue to accept
one cancellation request and defer it to the next safe checkpoint before
physical version cleanup. Registration removal is committed before checked
reduced-index publication, and partial logical commits are reported for retry.
Startup evidence writers never wait for retention-specific locks. Evidence
published after its observation is
preserved by exact, same-file-object compare-before-delete cleanup. Cleanup also
requires the opened file's handle-resolved parent to match the observed
`.launch` directory, so a later intermediate junction or symlink cannot
redirect deletion. Later evidence does not roll back an already published
database.
Operators accept the availability risk that reclaimed dormant applications may
later require a download or fail permanently under offline/no-download policy.
## Known TOCTOU Races
These check-then-use patterns have inherent race windows. They are mitigated
but not eliminated. Document any new ones here.
| Check | Use | Location | Mitigation |
|-------|-----|----------|------------|
| `IsReparsePoint()` | `MoveFileExW()` (install) | `installer_file_ops.cc` `InstallVersion()` | Same-volume atomic rename minimizes window |
| `IsReparsePoint()` | `MoveFileExW()` (uninstall) | `installer_file_ops.cc` `UninstallVersion()` | Same-volume atomic rename |
| `IsReparsePoint()` | `base::CopyFile()` (relaunch) | `installer_relaunch.cc` | 64-bit random makes prediction impractical |
| `PathExists(catalog)` | `VerifyCatalogSignature()` | `installer_signature.cc` `VerifyWithCatalog()` | Catalog from just-extracted temp dir under installer control |
| `GetFileSize()` | `VerifyFileHash()` | `installer_download.cc` `DownloadFile()` | Low risk: file just written by this process in temp dir |
| `TryUseDirectory()` write test | Actual file writes | `installer_paths.cc` | Write test only checks capability; real writes are under lock. Explicit uninstall captures the selected mutation target in its prepared snapshot. |
| `GetPhysicalPathContainment()` | Choose direct mutation or temporary relaunch | `installer_paths.cc`, `installer_bootstrap_helpers.cc`, `bootstrap_win.cc` | Both identities are handle-resolved and indeterminate results fail closed. The immutable preflight binds the selected target; downstream reparse checks and the writer lock limit later mutation races. |
| `IsReparsePoint(cache_dir)` | `WriteFile()` / `ReadFileToString()` | `installer_download.cc` `DownloadWithCache()` | Best effort — attacker already has user-level access; code signing is the hard boundary |
| `IsReparsePoint(cache_path)` | `WriteFile()` | `installer_download.cc` `DownloadWithCache()` | Content is CDN-sourced (HTTPS), not attacker-controlled |
| `IsReparsePoint(path)` | `WriteFile()` / `ReadFileToString()` | `installer_revocation.cc` `WriteRevocationCache()`, `LoadRevocationCache()` | Additive-only merge means injected entries cause DoS, not code execution |
| `IsReparsePoint(versions_dir)` | `FileEnumerator` / `MoveFileExW` | `installer_file_ops.cc`, `installer_paths.cc`, `installer_version_metadata.cc` | Same-privilege; attacker must replace Versions/ dir between check and use |
| `IsDirectorySafe()` | `MoveFileExW()` | `installer_file_ops.cc` `InstallVersion()` | Lock prevents concurrent modification by other installer instances |
| `VerifySafeDirectoryPath()` | `CreateDirectory()` (staging, cache) | `installer_controller.cc` `DownloadAndInstall()` | Same-privilege; lock prevents concurrent installer instances |
| `VerifySafeFilePath()` | `DownloadFile()` (archive) | `installer_controller.cc` `DownloadAndInstall()` | Content is verified post-download (signature + hash) |
| `IsPathSafeForLoading()` | `LoadLibrary()` by client | `installer_file_ops.cc` / `installer_controller.cc` `MakeValidatedLibcefResult()` | Accepted at the owning user's trust level for user-writable stores. The cefclient reference loader's optional load-time `libcef.dll` signer check narrows this race as defense-in-depth; arbitrary clients must opt in, and catalog-covered dependencies/resources remain inside the same-user boundary. |
---
## Testing Mode Guards
These features exist for testing but MUST be compiled out of official builds
or reject requested test state in official builds:
| Feature | Effect | Guard | File |
|---------|--------|-------|------|
| `SetTestingMode()` / `IsTestingMode()` | Master toggle; enables HTTP downloads, ignores TLS certificate errors, suppresses error dialogs | `OFFICIAL_BUILD && NDEBUG` | `installer_controller.cc` |
| `SetStringResourceLoaderForTesting()` / `SetMessageBoxRunnerForTesting()` | Replaces localized UI strings or error-dialog display | `OFFICIAL_BUILD && NDEBUG` (callbacks and setters are compiled out) | `installer_progress_dialog.cc` |
| `SetUninstallRelaunchCallbacksForTesting()` | Replaces process launch, lifecycle delivery, and authenticated client-module copying for deterministic relaunch tests | `OFFICIAL_BUILD && NDEBUG` (callbacks and setter are compiled out) | `installer_relaunch.cc` |
| `SetEmergencyRecoveryScanLimitsForTesting()` | Overrides bounded automatic-startup recovery root, entry, and time limits | `OFFICIAL_BUILD && NDEBUG` (override state and method are compiled out) | `installer_controller.cc` |
| `SetSignatureTestingMode()` | Accepts self-signed catalogs, skips thumbprint matching | `OFFICIAL_BUILD && NDEBUG` | `installer_signature.cc` |
| `allow_http_for_testing` in `DownloadOptions` | Allows HTTP URLs (set by `IsTestingMode()`) | `OFFICIAL_BUILD && NDEBUG` | `installer_download.cc` |
| `ignore_certificate_errors_for_testing` in `DownloadOptions` / `CEF_INSTALLER_IGNORE_CERTIFICATE_ERRORS_FOR_TESTING` | Ignores TLS certificate errors; set automatically by `IsTestingMode()` or explicitly by the environment variable for loopback process tests | `OFFICIAL_BUILD && NDEBUG` | `installer_controller.cc`, `installer_download.cc` |
| `OverrideInstallDirectoriesForTesting()` | Overrides readable/writable directory discovery | `OFFICIAL_BUILD && NDEBUG` | `installer_paths.cc` |
| `ResolveInstallDirectories()` override path | Returns test dirs instead of registry/filesystem | `OFFICIAL_BUILD && NDEBUG` | `installer_paths.cc` |
| `InstallerE2EConfig` | Parses all `CEF_INSTALLER_TEST_*` subprocess inputs once into typed, process-wide settings; product components never read those environment variables directly | `OFFICIAL_BUILD && NDEBUG` (fault settings are inert; requested directory overrides are rejected) | `installer_e2e_config.cc` |
| `CEF_INSTALLER_TEST_DIRECTORY_SCENARIO=admin_mutation_denied` with `CEF_INSTALLER_TEST_DIRECTORY_ROOT` | For original-uninstall E2E coverage, replaces directory candidates, reports elevation, and closes the admin mutation gate during uninstall preflight | `OFFICIAL_BUILD && NDEBUG` (requested test state is rejected) | `installer_e2e_config.cc`, `installer_bootstrap_helpers.cc`, `installer_paths.cc` |
| `CEF_INSTALLER_TEST_RELAUNCH_FAILURE=1` | Forces required uninstall relaunch to fail before publication or child creation | `OFFICIAL_BUILD && NDEBUG` (setting is inert) | `installer_e2e_config.cc`, `installer_relaunch.cc` |
| `CEF_INSTALLER_TEST_CHILD_CONFIG_FAILURE=1` | Fails a marked child after trusted state acceptance at the config-loading boundary | `OFFICIAL_BUILD && NDEBUG` (setting is inert) | `installer_e2e_config.cc`, `installer_bootstrap_helpers.cc` |
| `CEF_INSTALLER_TEST_CHILD_STATE_BARRIER=<path>` | Signals a fixture-owned marker immediately after trusted-state acceptance and waits up to 30 seconds for deterministic termination testing | `OFFICIAL_BUILD && NDEBUG` (setting is inert) | `installer_e2e_config.cc`, `installer_bootstrap_helpers.cc` |
| `CEF_INSTALLER_TEST_DATABASE_SAVE_FAILURE`, `CEF_INSTALLER_TEST_INDEX_FAULT`, `CEF_INSTALLER_TEST_FILE_OPS_FAULT` | Injects typed persistence and mutation-boundary failures in subprocess/E2E tests | `OFFICIAL_BUILD && NDEBUG` (settings are inert) | `installer_e2e_config.cc`, `installer_database.cc`, `installer_version_metadata.cc`, `installer_file_ops.cc` |
| `certificate_thumbprint` in an embedded test resource / `Config` | Overrides the certificate thumbprint for archive verification; auto-enables `SetSignatureTestingMode` when the thumbprint differs from production default | `OFFICIAL_BUILD && NDEBUG` | `installer_config.cc`, `bootstrap_win.cc` |
| `/cef-install-path` CLI flag | Overrides the CEF installation directory; could redirect installs to attacker-controlled paths | `OFFICIAL_BUILD && NDEBUG` | `installer_bootstrap_helpers.cc` |
| `/cef-download-path` CLI flag, `local_download_path` in `DownloadOptions` and `ExtendedConfig` | Reads manifests and archives from a local directory instead of CDN; bypasses network security (HTTPS, certificate pinning) for the transport layer. All post-download validation (hash, signature, size limits) still applies | **No guard (allowed in official builds)** — signature verification is the security boundary, not HTTPS transport | `installer_bootstrap_helpers.cc`, `installer_download.cc`, `installer_controller.h` |
`Logger::ResetForTesting()` and `ProgressDialog::FlushForTesting()` have no
`OFFICIAL_BUILD` guard but are not security-sensitive (they only reset internal
logging state or drain pending tasks on the internal dialog thread,
respectively).
`ScopedThreadPool` intentionally uses `ThreadPoolInstance::JoinForTesting()`
and `FeatureList::ResetEarlyFeatureAccessTrackerForTesting()` in official
builds. The installer creates a temporary thread pool only when the embedding
process has not initialized one, and must tear it down before that process may
later initialize Chromium. These are lifecycle operations, not configurable
testing bypasses.
If adding a new testing bypass, it MUST use the same `OFFICIAL_BUILD && NDEBUG`
guard pattern and MUST be documented in this table.
+906
View File
@@ -0,0 +1,906 @@
# Installer Testing Guide
All commands below run from the `chromium/src` directory.
## Building Tests
The installer unit tests are defined in `//cef/BUILD.gn` as the
`cef_installer_unittests` target. Replace `<build_dir>` with your build
directory (e.g., `out/Debug_GN_x64`).
```bash
# Unit + integration tests only
autoninja -C <build_dir> cef_installer_unittests
# Everything (unit tests + E2E infrastructure + bootstrap binary)
autoninja -C <build_dir> cef cef_installer_unittests
```
## Running Tests
```bash
# Run all installer tests
<build_dir>/cef_installer_unittests.exe
# Run a specific test suite
<build_dir>/cef_installer_unittests.exe --gtest_filter="InstallerConfig*"
# Run a specific test
<build_dir>/cef_installer_unittests.exe --gtest_filter="InstallerArchiveTest.ExtractValidArchive"
# List all available tests
<build_dir>/cef_installer_unittests.exe --gtest_list_tests
```
## Directory roles and provisioning
`InstallerPathsTest` uses source-tagged candidate overrides, injected physical
read/write state, and injected elevation so the HKLM, Program Files, per-user,
and custom matrix does not depend on the runner's token or host ACLs. The
controller tests additionally override the official admin-mutation decision to
prove gated startup takes no writer lock, database/registration path, or
per-user fallback. Focused filters are:
```bash
<build_dir>/cef_installer_unittests.exe \
--gtest_filter="InstallerPathsTest.*Role*:InstallerPathsTest.*Admin*:InstallerPathsTest.*Elevated*:Controller*.*AdminDirectory*"
<build_dir>/cef_installer_unittests.exe \
--gtest_filter="InstallerConfigTest.*InstallPath*:ConfigPrecedenceTest.*InstallPath*"
```
The resource fixture `cef_config_test_appid_a.dll` contains a relative
`install_path`; helper tests verify client-only acceptance, DLL-relative
resolution, and independent bootstrap provenance for `enable_explicit_modes`.
Real Program Files/HKLM permission coverage requires an appropriately elevated
Windows environment. Broad E2E role tests should use isolated roots and the
non-official override; cleanup must be limited to paths/registry values created
by that test.
Provisioning uses existing `/cef-update` and `/cef-uninstall` commands with a
dedicated appid and exact `vmin = vmax` pin. Multiple pins use multiple appids.
Inspect `installer.json`, `versions.json`, `.launch`, `.cache`, `.staging`, and
`.trash` under only the selected test root when asserting side effects.
## E2E Tests
End-to-end tests exercise the real `bootstrap.exe` binary as a subprocess,
covering command-line parsing, config loading, exit codes, launcher mode, and
process lifecycle.
### Building
E2E tests require the full `cef` target (includes `bootstrap.exe`, the mock
client DLL, the progress helper, and the CDN builder):
```bash
autoninja -C <build_dir> cef
```
### Prerequisites
- **`CEF_RESOURCE_HACKER_PATH`** (system environment variable) — Must point to
`ResourceHacker.exe` for tests that add or replace embedded configuration
resources, including E2E2 and the bootstrap/client-resource CDN failover
tests. Those tests fail with a clear error if it is not set. Tests that do
not modify embedded resources do not require ResourceHacker.
- **Windows certificate-store access** — The E2E CDN builder creates signed
test distributions, and the suite launches helper binaries. Sandboxed runs
may fail with certificate or key-storage errors. Rerun the E2E command with
the required elevated permission instead of changing product code.
- **Loopback HTTPS assets** - The self-signed localhost certificate and its
test-only private key are checked in under `testdata`; OpenSSL is not a test
prerequisite. The private key is fixture data and must never be used outside
the installer tests.
- **Enterprise policy fixture access** - `test_enterprise_policy.py` creates
one unique subkey below
`HKCU\\SOFTWARE\\CEF\\InstallerPolicyTests` by default, explicitly in the
shared 64-bit view, so ordinary non-administrator runs exercise real
registry I/O. Non-official binaries accept only that test-root prefix and
the `HKCU`/`HKLM` test-hive values; official builds ignore both seams. Set
`CEF_INSTALLER_E2E_POLICY_HKLM=1` in an elevated environment to run the same
cases below `HKLM` instead. The test removes only its UUID-named key and
never replaces or deletes the real `HKLM\\SOFTWARE\\Policies\\CEF` key.
### Running
```bash
# Run all E2E tests
python3 cef/libcef_dll/bootstrap/installer/e2e/run_e2e_tests.py \
--build-dir=<build_dir>
# Run a single E2E test
python3 cef/libcef_dll/bootstrap/installer/e2e/run_e2e_tests.py \
--build-dir=<build_dir> \
-k test_install_flow.TestInstallFlow.test_fresh_install
```
### Uninstall routing coverage
The uninstall unit and E2E tests use exact exit codes and inspect final logical
state:
- An external executable with a writable target is synchronous (0 on success),
including a client-resource relative custom path and the two-app prune case.
- Database/index failures return their exact nonzero controller code; physical
cleanup failure returns 0 after logical commit and leaves cleanup deferred.
- Resolver/preflight unit tests cover read-only exclusive custom/admin targets:
they return the normal no-writable error without a lower-priority fallback.
A containment unit test applies a scoped empty DACL to a temporary file, and
the CLI E2E applies a scoped write-deny DACL to an owned custom store. The E2E
verifies the normal error, unchanged state, and no temporary child.
- Physical containment uses handle-resolved `FilePath` equality/parent checks.
A real case-sensitive directory fixture verifies that sibling `CEF` and
`cef` components remain distinct.
- With a writable target, the missing-executable ordinary-uninstall preflight
test verifies an exact 105 decision before mutation or relaunch. A separate
real ACL-inaccessible fixture verifies that the containment helper returns
`kIndeterminate`.
- A non-official, accepted-value-only directory-role E2E seam models an
elevated admin store whose mutation gate is closed. The subprocess test
verifies no per-user fallback, mutation, or temporary child.
- An executable staged beneath the writable target, or an external executable
using `/cef-background`, returns exactly 109. With a pumped `/cef-parent`,
lifecycle E2E tests correlate the handoff and normalized terminal result.
Filesystem polling remains only legacy test cleanup and is not a supported
caller completion contract.
- Prepared-controller tests verify that snapshots bound to relaunch or reject
decisions cannot be passed to the direct controller entry point.
- Forced relaunch failure returns 105 with state unchanged. A marker without
valid trusted state, or relaunch transport on a non-uninstall command,
returns 100 without mutation.
Use these focused commands from `//src`:
```powershell
out\Debug_GN_x64\cef_installer_unittests.exe `
--gtest_filter='BootstrapHelpersTest.*Uninstall*:InstallerPathsTest.*Contain*:InstallerRelaunchTest.*:Controller*.*Uninstall*:InstallerOutcomeTest.*'
python cef\libcef_dll\bootstrap\installer\e2e\run_e2e_tests.py `
--build-dir out\Debug_GN_x64 `
-k 'test_uninstall or exit_code_relaunched or install_path_relative_in_client_dll_resource or trash_reclamation_reports_cleanup_deferred' `
--verbose
out\Debug_GN_x64\cef_installer_unittests.exe `
--gtest_filter='InstallerLifecycleTest.*:ProgressUIIntegrationTest.*:InstallerRelaunchTest.*'
python cef\libcef_dll\bootstrap\installer\e2e\run_e2e_tests.py `
--build-dir out\Debug_GN_x64 `
-k 'lifecycle or progress or uninstall' `
--verbose
```
Run lifecycle race/correlation filters at least 20 times when changing the
transport. Unit and E2E binaries require Windows certificate-store access;
rerun outside a restricted sandbox when certificate/key-storage APIs are
denied. Elevation-specific UIPI coverage is environment-dependent: a
higher-integrity receiver must opt in to `WM_COPYDATA`; same-integrity delivery
is always covered.
`/cef-uninstall-relaunched` is internal transport/test state, not a supported
caller bypass. It is accepted only for explicit uninstall with the nonce-bound
trusted state file created beside the temporary executable. The non-official
`CEF_INSTALLER_TEST_RELAUNCH_FAILURE=1` environment seam deterministically
exercises failure before temp publication or child creation.
The admin-gate E2E pairs
`CEF_INSTALLER_TEST_DIRECTORY_SCENARIO=admin_mutation_denied` with an absolute
`CEF_INSTALLER_TEST_DIRECTORY_ROOT` containing existing `Admin` and `PerUser`
directories. It is accepted only for an original uninstall in non-official
builds; missing, unknown, relative, or incomplete values return configuration
error, and official release builds reject requested test state.
### E2E test infrastructure
- **`e2e_test_base.py`** — Base class with helpers for running the installer,
building local CDN directories, reading the database, embedding config
resources via ResourceHacker, and launch state assertions. Each test
unconditionally restores its fixed-name executable from unmodified
`bootstrap.exe` before resource edits; launcher helpers likewise restore the
executable and client DLL they own. Resource-editing tests require
`CEF_RESOURCE_HACKER_PATH`, while the missing-resource test launches the
freshly restored executable without invoking Resource Hacker.
- **`cef_e2e_mock_client.dll`** — Mock client DLL for launcher mode tests.
Supports single-threaded mode (env: `CEF_E2E_CONFIG_JSON`) and multi-threaded
mode (env: `CEF_E2E_THREADING_MODE=multi`, `CEF_E2E_CONFIG_JSON_A/B`).
When `CEF_E2E_EXIT_CODE` is set, `RunWinMain` writes the marker file and
returns the specified exit code without calling `RunInstaller` — this lets
launch health tests cover default/missing `off`, `explicit`, and
`exit_code` modes, including clean exits, crashes, neutral exits, prior-boot
records, explicit confirmation, and atomic write failure behavior.
Controller unit tests cover invalid-file GC and writer-locked reparse-point
repair.
When `CEF_E2E_LAUNCH_SUCCESS` is also set, the mock client calls
`RunInstaller("launch_success", ...)` to confirm launch health *before*
writing the marker file and returning the exit code. This lets launch health
tests simulate explicit confirmation before clean exits or crashes.
- **`cef_e2e_progress_helper.exe`** — Creates a message-only window. Its
legacy invocation writes the existing progress JSON list. `--lifecycle`
writes separate raw `progress` and `lifecycle` arrays plus a rejected-message
count. `--lifecycle-return=N`, `--non-pumping`,
`--close-after-handoff`, and `--allow-copydata` cover receiver return,
timeout, destruction, and UIPI opt-in behavior. The window procedure only
bounds and copies bytes; parsing occurs afterward.
- **`lifecycle_receiver.py`** — Strict version-1 reference parser and
operation-ID correlator. It buffers terminal-first delivery, leaves missing
handoffs unattributed, rejects non-terminal/unknown exit codes, ignores
malformed/future events, and preserves the first valid correlated terminal.
State is capped at 256 operations, expires after 30 idle minutes, and can
be consumed or explicitly removed.
- **`cef_e2e_build_test_cdn.exe`** — Builds signed test distributions in CDN
layout.
Marker JSON from `cef_e2e_mock_client.dll` records size-gated
`installer_error_code` and `installer_error_message`; a null message remains
JSON `null`.
### Test-only flags
See [Testing Mode Guards](SECURITY.md#testing-mode-guards) for the
official-build requirements that prevent test seams from becoming production
bypasses.
All `CEF_INSTALLER_TEST_*` environment variables are parsed centrally into
typed `InstallerE2EConfig` state. The owning directory, relaunch, config,
database, index, or file-operation component applies the requested fault;
bootstrap orchestration does not interpret test scenarios.
- **`/cef-install-path=<path>`** — Override the CEF installation directory. Each
E2E test uses its own temp directory for isolation. Available only in
non-official builds.
- **`/cef-log-level=<level>`** — Set minimum log level for `cef_installer.log`.
Accepted values: `info`, `warning`, `error` (case-insensitive). Default:
`warning`. Use `/cef-log-level=info` for verbose output when debugging.
Available in all builds.
- **`CEF_INSTALLER_TEST_CHILD_CONFIG_FAILURE=1`** — Fails only a marked child
after trusted state is accepted at the config-loading boundary, producing a
controlled config terminal.
- **`CEF_INSTALLER_TEST_CHILD_STATE_BARRIER=<absolute path>`** — Writes a
fixture-owned ready marker after state acceptance and waits for up to 30
seconds while it exists, allowing deterministic forced termination.
- **`certificate_thumbprint`** (embedded test resource, non-official builds
only) — Override the certificate thumbprint used for archive verification.
When set to the test cert thumbprint, the bootstrap auto-enables signature
testing mode so self-signed archives pass verification.
Lifecycle teardown terminates only the exact helper/child processes started by
the fixture, waits for them, and removes only fixture-owned markers and
verified new relaunch directories. Missing terminal delivery is indeterminate;
tests do not infer completion from database, index, or temporary-directory
internals.
### Writing E2E tests — common pitfalls
**`CEF_E2E_LAUNCH_SUCCESS` and `CEF_E2E_EXIT_CODE` interact.** Setting both
simulates an app that confirms CEF health then exits with the given code. The
sentinel is confirmed (`running=false, failures=0`) during `RunWinMain`, so a
subsequent crash exit does *not* count as a CEF failure (the post-exit handler
doesn't run for a failure exit, and the confirmation is already durable).
Without `CEF_E2E_LAUNCH_SUCCESS`, a non-zero (failure) exit leaves the sentinel
as `running=true`, which is detected as a crash on the next launch. Use the
`launch_success=True` parameter on `_run_launcher_with_exit_code` to set it.
Note that confirming on a *clean* exit still triggers post-exit cleanup/prune,
which can delete an older confirmed version's `.launch/` file (and the version)
— confirm on a crash exit if you need an older fallback version to survive.
**`_standalone_install` has side effects.** Each standalone install runs
`ComputeAndInstall` → `DownloadAndInstall` → `PruneUnusedVersions`. The
prune age-checks valid orphaned and below-global-`vmin` `.launch/` files.
Records less than 90 days old survive; records exactly 90 days old or older
may be deleted. E2E fixtures simulate age with integrity-protected
`last_update` (health) or `last_launch` (liveness) content. Do not use
filesystem mtime to age a fixture.
**Global vmin is the minimum across ALL registered apps.** If you need a
`.launch/` file to survive a standalone install that raises one app's vmin,
register a second app with a lower vmin first (e.g., `appid2` with
`vmin=v1`). This keeps the global minimum low.
**`_standalone_install(version, vmin=X)` won't install `version` if a
version ≥ X is already installed.** The installer selects the best existing
version that satisfies vmin. To force a specific version, use `vmin=version`
(or `vmin=version, vmax=version` to pin).
**`_embed_launcher_config` writes to the exe, not the DLL.** The bootstrap
reads config from the client DLL first; the exe resource is a fallback. In
launcher mode with the mock client DLL (which has no embedded config), the
exe resource IS used. But if you've embedded config into the DLL (e.g., for
bundled path tests), the DLL config takes priority and the exe config is
ignored.
**Cleanup vs GC — test the right mechanism.** On successful exit, the
bootstrap deletes `.launch/` files via `cleanup_paths` (only confirmed older
files for the current app). Separately, `PruneUnusedVersions` runs stale
file GC for orphaned and below-global-vmin records after the 90-day content-age
grace. If your test asserts a file was deleted, make sure it was deleted by the
mechanism you intend to test, not the other one. Use the second-app trick above
to neutralize GC when testing cleanup, or keep vmin constant.
**Launch GC has a dedicated unit-test clock.** Tests that call
`SetLaunchStateGcTimeForTesting()` must reset it to `std::nullopt` in fixture
teardown. Keep health `last_update` independent from retention's
`pid_start_time`; liveness uses `last_launch` for both policies. Useful focused
checks are:
```powershell
out\Debug_GN_x64\cef_installer_unittests.exe `
--gtest_filter="LaunchStateTest.*:ControllerTempDirTest.Pruning_*"
python cef\libcef_dll\bootstrap\installer\e2e\run_e2e_tests.py `
--build-dir out\Debug_GN_x64 -k launch_health --verbose
```
**Pruning requires a version to not be the best match.** A version is only
prunable if no registered app's best match points to it. Installing a newer
version doesn't make the older one prunable if the older one is still the
best match (e.g., because vmin hasn't changed). Unregistering the app or
installing a strictly better version makes the old one prunable.
## Security
See [SECURITY.md](SECURITY.md) for the security invariants that tests must
cover. When adding a new code path (e.g., a new extraction method), check the
"If you change" sections in SECURITY.md for required test coverage.
## Test Conventions
Each production source file has a corresponding `*_unittest.cc` (e.g.,
`installer_archive.cc` -> `installer_archive_unittest.cc`). Integration tests
that require a mock CDN server use the `InstallerIntegrationTest` fixture in
`installer_integration_test.cc` (full `Controller::Run` with embedded HTTP
server). Config precedence unit tests (`TryLoadInstallerConfig`) use test DLLs
with embedded resources (`cef_config_test_appid_a.dll`, etc.) loaded via
`LoadLibraryEx(LOAD_LIBRARY_AS_DATAFILE)`.
The `cef_config_test_malformed.dll` and `cef_config_test_invalid.dll`
fixtures verify that present-but-bad trusted resources do not fall back.
`embed_raw_config_resource()` provides the equivalent E2E path.
The CDN-list resource fixtures also include valid client/bootstrap lists and
invalid HTTP, wrong-shape, empty, over-count, and over-length lists. Keep these
fixtures in the public config-loader matrix: application `cdn_urls` use normal
client/bootstrap source selection and must never merge.
Failure and API contract focused validation:
```bash
<build_dir>/cef_installer_unittests.exe --gtest_filter="InstallerConfigTest.*:ConfigPrecedenceTest.*:InstallerResultTest.*:InstallerExitCodeTest.*:InstallerOutcomeTest.*:ExtendedConfigTest.*:RunInstallerTest.*:InstallerStartupResultTest.*"
python3 cef/libcef_dll/bootstrap/installer/e2e/run_e2e_tests.py \
--build-dir=<build_dir> \
-k "test_run_installer or test_launcher_mode or test_exit_codes"
```
Resolver and transactional-correctness focused validation:
```bash
<build_dir>/cef_installer_unittests.exe --gtest_filter="InstallerVersionResolverTest.*:VersionIndexTest.*:InstallerRevocationTest.*:InstallerFileOpsTest.*Repair*:ControllerTempDirTest.*Index*:ControllerTempDirTest.*Rebuild*:ControllerTempDirTest.*Orphan*:InstallerIntegrationTest.*Update*:InstallerIntegrationTest.*Repair*"
```
Registration-retention focused validation:
```bash
<build_dir>/cef_installer_unittests.exe \
--gtest_filter="InstallerRetentionTest.*:LaunchStateTest.*Retention*:ControllerTempDirTest.Retention*:BootstrapHelpersTest.*Retention*:RetentionOutputTest.*:InstallerFileIntegrityTest.*Conditional*:ProgressDialogTest.*Cancel*"
python3 cef/libcef_dll/bootstrap/installer/e2e/run_e2e_tests.py \
--build-dir=<build_dir> \
-k test_retention
```
CDN robustness focused validation:
```powershell
out\Debug_GN_x64\cef_installer_unittests.exe `
--gtest_filter='InstallerConfigTest.*Cdn*:ConfigPrecedenceTest.*Cdn*:ExtendedConfigTest.*Cdn*:CombinedConfigTest.*Cdn*:ExtendedConfigSecurityTest.*Cdn*:InstallerPolicyTest.*Source*:InstallerPolicyTest.*Url*:InstallerPolicyTest.*Identity*:InstallerDownloadTest.*ContentRange*:InstallerDownloadTest.*Partial*:InstallerDownloadTest.*Prune*Archive*:InstallerDownloadServerTest.*Range*:InstallerDownloadServerTest.*Partial*:InstallerDownloadServerTest.*Origin*:InstallerDownloadServerTest.*Promotion*:InstallerControllerServerTest.*CdnFailover*:InstallerControllerServerTest.NextBest*:InstallerControllerClassificationTest.NextBest*:InstallerControllerTest.NextBest*:InstallerIntegrationTest.*PolicyFailover*:InstallerIntegrationTest.*NextBest*:InstallerIntegrationTest.*ArchiveCleanup*:InstallerIntegrationTest.Background*:BootstrapHelpersTest.*Background*:InstallerParallelXzTest.*Background*:DetermineThreadCountTest.*Background*'
python cef\libcef_dll\bootstrap\installer\e2e\run_e2e_tests.py `
--build-dir out\Debug_GN_x64 `
--filter 'next_best_when_newest_archive_missing or explicit_background_update or authorized_application_cdn_https_failover or bootstrap_resource_cdn_https_failover or client_resource_cdn_https_failover'
```
Range tests use deterministic HTTP handlers that inspect exact request headers
and script `200`, `206`, `416`, duplicate/malformed `Content-Range`, length
disagreement, short/overlong bodies, redirects, and request counts. They must
assert that rejected responses are never promoted, interruption/cancellation
retains only the matching origin-bound partial, progress begins at the retained
size, and the protocol/final-origin/hash conditions share one clean-request
budget. Small manifest, revocation, and sidecar downloads remain non-resumable.
The signed integration tests cover one next-best selection without manifest
refetch, ordinary commit cleanup, archive retention after checked-index or
registration failure, post-commit `cleanup_deferred`, and propagation-lag
fallback. The local-CDN E2E builder can be called twice to advertise two
compatible versions; deleting only the newer archive exercises the same
fallback in a real bootstrap process.
Retention tests should verify the report as well as the durable state. Check
`installer.json`, `versions.json`, canonical files under `.launch`, and
`retention_pending.json`. Failure and cancellation cases must preserve the
documented database-before-index ordering, retain an exact pending version
scope when retry is required, and converge on an explicit retry.
`SetVersionIndexFaultForTesting()` deterministically injects checked index
write, replace, reread, and intended-set validation failures. Recovery tests
use explicit missing/corrupt/valid index snapshots plus staging, trash, and
unindexed directories; assertions must cover the durable index, registration,
directory membership, `outcome`, numeric `error_code`, symbolic `error_name`,
and `warnings` for deferred physical cleanup. Run crash/fault filters more than
once when changing publication order.
`SetFileOpsFaultForTesting()` covers quarantine/replacement moves and trash move
and reclamation. A quarantine-move failure preserves the existing target and
verified staging at their original paths. A replacement-move failure preserves
verified staging, leaves the old target under `.trash/`, and leaves the
canonical destination absent. Recovery must report post-index orphan cleanup
failures as successful `cleanup_deferred`; collision reclamation is likewise
`cleanup_deferred` only after directory and checked index publication succeed.
- Use `base::ScopedTempDir` for filesystem operations.
- Use `Version::FromApiVersion(CEF_API_VERSION_LAST)` for version values to
avoid vmin clamping.
- Use `SetTestingMode(true)` in non-official tests to permit an already-parsed,
internally injected HTTP source and suppress error dialogs. Public resource
and `RunInstaller` `cdn_urls` parsing remains HTTPS-only.
- `VersionMetadata::IsValid()` requires non-empty `version`, `abi_hash`, and
`platform`.
## Manual Testing
There are three ways to manually test the bootstrap + installer, each
exercising a different CEF loading path. All three require a Release build
(see [Prerequisites](README.md#prerequisites)).
In the examples below, `<build_dir>` is your build output directory
(e.g., `out/Release_GN_x64`) and `cefclient` is the client name.
The bootstrap binary is always renamed to match the client DLL (e.g.,
`bootstrap.exe` → `cefclient.exe`, which loads `cefclient.dll`).
**`enable_explicit_modes`:** Explicit commands (`/cef-update`,
`/cef-uninstall`) and standalone auto-install require
`"enable_explicit_modes": true` in the bootstrap exe's embedded
`CEF_INSTALLER_CONFIG` resource (official builds only — non-official builds
allow these unconditionally). See
[Embedding Resources with ResourceHacker](#embedding-resources-with-resourcehacker)
for how to add this post-build.
### Mode 1: Unchecked CEF Path (all binaries co-located)
The simplest setup — all binaries live in one directory. This is the default
when `cefclient.dll` has an `unchecked_cef_path` config (typically `"."`) or
no `CEF_INSTALLER_CONFIG` resource at all.
**Setup:** No extra steps needed. The build output already has `cefclient.exe`,
`cefclient.dll`, `chrome_elf.dll`, `libcef.dll`, and all CEF resources in the
same directory.
```
<build_dir>/
cefclient.exe # bootstrap.exe, renamed
cefclient.dll
chrome_elf.dll # Must match exe signing (same cert, or both unsigned)
libcef.dll
*.pak, locales/, ... # CEF resources
```
**Run:**
```
<build_dir>/cefclient.exe
```
The bootstrap finds `libcef.dll` adjacent to the client DLL via
`unchecked_cef_path` and skips the installer entirely — no version checks,
no signature verification, no CDN access.
### Mode 2: Bundled CEF Path (CEF in a subdirectory)
Tests the bundled version selection path. The bootstrap, client DLL, and
`chrome_elf.dll` are in one directory; the CEF distribution is pre-extracted
into a subdirectory.
**Setup:** Create a directory with the bootstrap files, and extract a signed
CEF distribution into a subdirectory (e.g., `cef/`):
```
test_dir/
cefclient.exe # Copy of bootstrap.exe, renamed
cefclient.dll # Client DLL with bundled_cef_path config
chrome_elf.dll # Must match exe signing (same cert, or both unsigned)
cef/ # Extracted CEF distribution
cef_version.json
catalog.cat
Release/
libcef.dll
chrome_elf.dll
*.pak, locales/, ...
```
The client DLL's `CEF_INSTALLER_CONFIG` resource should include:
```json
{
"appid": "...",
"vmin": "...",
"abi_hash": "...",
"bundled_cef_path": "cef"
}
```
The `bundled_cef_path` is resolved relative to the client DLL's directory.
**Run:**
```
test_dir/cefclient.exe
```
The bootstrap finds the bundled version via `bundled_cef_path`, validates it
against version/ABI constraints, and loads it in-place without any CDN access.
### Mode 3: Local Download Path (install from archive)
Tests the full CDN download + install flow using a local directory instead of
the network. The `/cef-download-path` switch redirects all CDN requests to
local file reads. All post-download validation (hash verification, signature
checking, archive extraction, version filtering) runs normally.
**Setup:** Create a local directory simulating the CDN layout. The installer
derives filenames from the URLs it would normally fetch:
```
cdn_dir/
{abi_hash}_{platform}.json # Manifest (e.g., 1671cc913eeb4ecf_windows64.json)
cef_binary_151.1.0_windows64_signed.tar.xz # Signed archive
cef_binary_151.1.0_windows64_signed.tar.xz.sha256 # SHA256 hash (hex string, no filename)
```
**Manifest filename:** When `abi_hash` is set in the config, the manifest
filename is `{abi_hash}_{platform}.json`. Without `abi_hash`, it is
`{milestone}_{platform}.json` (e.g., `151_windows64.json`).
The manifest is a JSON array of build entries. The example uses the current
CEF 151.1 sandbox compatibility hash; replace the archive digests and timestamp
with values from the actual test artifact.
```json
[
{
"version": "151.1.0",
"file": "cef_binary_151.1.0_windows64_signed.tar.xz",
"sha1": "70e10aff7a7b346ed24c3eb24c04b503b3f61f29",
"last_modified": "2026-07-15T00:00:00Z",
"abi_hash": "1671cc913eeb4ecf"
}
]
```
The `.sha256` file contains just the hex hash (no filename):
```
2a4b43a1936e0d56511b98cf1ae497e8439710a2412e57379560561ab3f86124
```
**Run:**
```bash
# With a client DLL (installer resolves CEF before loading the client)
cefclient.exe /cef-download-path=C:\tmp\cdn_dir
# Standalone mode (no client DLL, requires a bootstrap exe resource)
cefclient.exe /cef-download-path=C:\tmp\cdn_dir
# With verbose logging (default log level is warning)
cefclient.exe /cef-download-path=C:\tmp\cdn_dir /cef-log-level=info
```
### Embedding Resources with ResourceHacker
You can add or update `CEF_INSTALLER_CONFIG` resources post-build using
[ResourceHacker](http://www.angusj.com/resourcehacker/):
1. Create `config.json`. Use the same `appid`, `vmin`, and `abi_hash` as the
client DLL's config so the exe registers/unregisters the same application.
`appid` and `vmin` are required — the config parse fails without them.
```json
{
"appid": "...",
"vmin": "...",
"abi_hash": "...",
"enable_explicit_modes": true
}
```
2. Create `config.rc`:
```rc
CEF_INSTALLER_CONFIG RCDATA "config.json"
```
3. Compile and embed (run from the directory containing both files):
```bash
ResourceHacker.exe -open config.rc -save config.res -action compile
ResourceHacker.exe -open cefclient.exe -save cefclient.exe -action addoverwrite -res config.res -mask RCDATA,CEF_INSTALLER_CONFIG,
```
The `CEF_RESOURCE_HACKER_PATH` environment variable should point to
`ResourceHacker.exe` (also used by E2E tests).
### Testing Explicit Commands
```bash
# Install CEF from a local directory first (see Mode 3 above)
cefclient.exe /cef-download-path=C:\tmp\cdn_dir
# Uninstall (unregisters the app and prunes unused versions)
cefclient.exe /cef-uninstall
# Explicit update check (checks even when a local version qualifies, and
# downloads only a newer compatible non-revoked version)
cefclient.exe /cef-update /cef-download-path=C:\tmp\cdn_dir
```
Registration retention is a separate, explicit maintenance operation. Start
with dry-run, review the reported registrations and version impact, and apply
only after confirming that dormant applications may need to download CEF again:
```powershell
# Deterministic text report; makes no installer-state changes.
cefclient.exe /cef-retention-dry-run /cef-max-age-days=180
# JSON report for scripts and automated verification.
cefclient.exe /cef-retention-dry-run /cef-max-age-days=180 /cef-headless
# Recomputes under the writer lock before committing removals.
cefclient.exe /cef-retention-apply /cef-max-age-days=180
```
Retention commands require `enable_explicit_modes` in the bootstrap's embedded
configuration. Verify that dry-run leaves `installer.json`, `versions.json`,
`.launch`, version directories, and `retention_pending.json` unchanged. After
apply, inspect `retry_required`, `registrations_committed`, `versions_pruned`,
and `warnings`; rerun apply when `retry_required` is true.
To test revocation, add a `revoked.json` to the local CDN directory:
```json
{
"revoked_versions": [
{ "version": "151.1.0", "reason": "testing revocation" }
]
}
```
Then uninstall any previously installed version and attempt a fresh install —
it should fail with "CDN version is revoked":
```bash
cefclient.exe /cef-uninstall
cefclient.exe /cef-download-path=C:\tmp\cdn_dir
```
Note: revocation does not block bundled installs (Mode 2). A revoked bundled
version is demoted but still used as a last resort when no other version is
available. See [Version Selection](README.md#version-selection) for details.
Check `cef_installer.log` in the install directory (e.g.,
`%LocalAppData%\CEF\cef_installer.log`) for detailed output. The install
directory also contains `installer.json` (registered apps) and
`versions.json` (installed versions) which can be inspected directly.
### Lock-free launch and concurrency
Focused unit coverage:
```powershell
out\Debug_GN_x64\cef_installer_unittests.exe `
--gtest_filter='InstallerLockTest.*:InstallerFileOpsTest.*VersionLease*:ControllerTempDirTest.*Query*:ControllerTempDirTest.AutomaticStartup*:InstallerControllerServerTest.AutomaticStartup*:LaunchStateTest.*Liveness*:InstallerRevocationTest.*Backoff*'
```
The tests use deterministic events rather than sleeps to pause a startup at the
writer transition. They cover query/local startup while the mutex is held,
post-lock index re-resolution with zero duplicate requests, contention timeout,
one abandoned handoff across three waiters, lease-blocked directory move,
fresh-cache zero requests, corrupt-cache refresh, stale refresh affecting the
current selection, post-writer refresh gating, strict failure-backoff
integrity, asynchronous cancellation while response headers are withheld,
asynchronous cancellation while a response body remains open, and liveness
refresh/clock-skew rules. Mutex coverage includes consecutive abandonment and
wrong-thread release rejection. The lease rename behavior is Windows-specific
and must be rerun for every locally available supported architecture; absence
of a 32-bit output directory is a validation limitation, not 32-bit coverage.
Emergency recovery unit tests use missing, corrupt, and valid-empty index
fixtures. They assert the selected lease, unchanged index state, query
exclusion, bad-CRC preservation, generic-read-error fail-closed behavior,
post-eligibility duplicate handling, held-mutex behavior, candidate rejection,
and each scan bound. `SetEmergencyRecoveryScanLimitsForTesting()` reduces
individual limits to zero for deterministic stopping tests. The launcher E2E
case `test_bootstrap_recovers_missing_and_corrupt_version_index` exercises both
damaged-index states in a real bootstrap/client process. See
[Version Selection](README.md#version-selection) for the production contract
and limits.
## Fuzz Testing
Archive extraction code processes untrusted data from the network. Three
libFuzzer targets provide continuous coverage-guided testing of the parsing
and decompression layers.
| Fuzzer | Target code | Attack surface |
|--------|-------------|----------------|
| `installer_xz_index_fuzzer` | `ParseXzIndex()`, `ReadMultiByteInt()` | XZ footer/index binary parsing, integer overflow, offset arithmetic |
| `installer_tar_fuzzer` | `ExtractTarFromBuffer()`, `TarReader::ParseHeader()`, `ParsePaxPath()` | Path traversal, PAX/GNU extensions, octal parsing, file writes |
| `installer_xz_block_fuzzer` | `DecompressXzBlock()` | LZMA2 block header parsing, decompression, CRC verification |
### Building Fuzzers
Create a fuzzer build directory by copying the existing debug config:
```bash
mkdir out/libfuzzer
cp out/Debug_GN_x64/args.gn out/libfuzzer/args.gn
```
Edit `out/libfuzzer/args.gn` and add or modify these settings:
```gn
# Changed from Debug_GN_x64:
is_component_build = false # was true; libfuzzer requires static linking
is_debug = false # was true; release build for fuzzer performance
# Added for fuzzing:
use_libfuzzer = true # enable libFuzzer instrumentation
is_asan = true # enable AddressSanitizer (catches buffer overruns, use-after-free)
is_ubsan_security = true # enable UBSan security checks (integer overflow, shift UB)
is_ubsan_vptr = false # vptr check unsupported on Windows
```
Then generate the build files:
```bash
gn gen out/libfuzzer
```
Keep all other settings from `Debug_GN_x64/args.gn` unchanged, such as
`target_cpu`, CDM keys, and codec flags. Only the six values above need to
change. UBSan's `vptr` check requires RTTI, which is disabled on Windows
(`/GR-`), so `is_ubsan_vptr` must be explicitly false.
Build the fuzzer targets:
```bash
autoninja -C out/libfuzzer installer_xz_index_fuzzer installer_tar_fuzzer installer_xz_block_fuzzer
```
### Running Fuzzers
```bash
# Run with seed corpus (recommended for the first run):
out/libfuzzer/installer_xz_index_fuzzer.exe cef/libcef_dll/bootstrap/installer/testdata/
# Run from an empty corpus:
out/libfuzzer/installer_tar_fuzzer.exe
# Run for a fixed duration:
out/libfuzzer/installer_xz_block_fuzzer.exe -max_total_time=300
# Reproduce a crash:
out/libfuzzer/installer_tar_fuzzer.exe crash-abc123...
```
Output lines containing `NEW` indicate that the fuzzer discovered inputs
covering previously unseen code edges. A crash writes the input to
`crash-<hash>` in the current directory.
### Design Notes
- The XZ index fuzzer neuters the CRC table, following the pattern in
`third_party/lzma_sdk/google/seven_zip_reader_fuzzer.cc`, so mutations are
not rejected at the CRC check before reaching deeper parsing logic.
- The tar fuzzer does not neuter checksums. Tar header checksums are simple sums
that the fuzzer discovers organically, and `ValidateChecksum()` is itself
worth fuzzing.
- The tar fuzzer caps input at 64 KB (`max_len=65536`) to keep iterations fast
because `ExtractTarFromBuffer` performs file I/O.
When adding a code path that parses untrusted archive data, add a corresponding
fuzzer target or extend an existing one to cover the new parsing logic. The
authoritative extraction invariants are in
[Archive Extraction](SECURITY.md#3-archive-extraction).
## Code Coverage
### Prerequisites
See [Coverage Setup Instructions](../../../tools/claude/CLAUDE_COVERAGE_INSTRUCTIONS.md)
for one-time setup (enabling coverage tools in `.gclient`, creating a coverage
build directory).
### Generating a Coverage Report
Coverage requires a separate build directory (`out/coverage`) with coverage
instrumentation enabled. Build the tests in that directory first:
```bash
autoninja -C out/coverage cef_installer_unittests
```
Then generate the report:
```bash
python3 tools/code_coverage/coverage.py \
cef_installer_unittests \
-b out/coverage \
-o out/report \
-c out/coverage/cef_installer_unittests.exe \
-f cef/libcef_dll/bootstrap/installer \
--coverage-tools-dir=third_party/llvm-build/Release+Asserts/bin \
--no-component-view
```
This produces:
- `out/report/win/summary.json` -- machine-readable summary
- `out/report/win/coverage.profdata` -- raw profile data
### Analyzing Coverage
The analysis commands below use the `analyze_coverage_output.py` script. The
`--profdata`, `--binary`, and `--llvm-cov` flags are needed for detailed
analysis (uncovered function names and line ranges); omit them for a quick
summary.
```bash
# Quick summary (filtered to installer files)
python3 cef/tools/claude/analyze_coverage_output.py \
out/report/win/summary.json \
--filter cef/libcef_dll/bootstrap/installer
# Detailed analysis (includes uncovered function names)
python3 cef/tools/claude/analyze_coverage_output.py \
out/report/win/summary.json \
--filter cef/libcef_dll/bootstrap/installer \
--profdata out/report/win/coverage.profdata \
--binary out/coverage/cef_installer_unittests.exe \
--llvm-cov third_party/llvm-build/Release+Asserts/bin/llvm-cov.exe
# Show uncovered line ranges for a specific file
python3 cef/tools/claude/analyze_coverage_output.py \
out/report/win/summary.json \
--profdata out/report/win/coverage.profdata \
--binary out/coverage/cef_installer_unittests.exe \
--llvm-cov third_party/llvm-build/Release+Asserts/bin/llvm-cov.exe \
--show-uncovered-lines cef/libcef_dll/bootstrap/installer/installer_controller.cc
# Show uncovered lines for all installer source files
python3 cef/tools/claude/analyze_coverage_output.py \
out/report/win/summary.json \
--profdata out/report/win/coverage.profdata \
--binary out/coverage/cef_installer_unittests.exe \
--llvm-cov third_party/llvm-build/Release+Asserts/bin/llvm-cov.exe \
--show-uncovered-lines "cef/libcef_dll/bootstrap/installer/*.cc"
```
For details on how `--show-uncovered-lines` computes coverage from LLVM regions,
see [How `--show-uncovered-lines` Works](../../../tools/claude/CLAUDE_COVERAGE_INSTRUCTIONS.md#how---show-uncovered-lines-works).
### Coverage Expectations
**Function coverage:** All production files should have 100% function coverage.
**Line coverage:** Overall line coverage should stay at or above 90%. Files that
currently have 100% line coverage (per `llvm-cov report`) must not regress. When
adding new code, add corresponding tests to maintain coverage.
**Acceptable exceptions from coverage:**
- **Singleton destructors** (e.g., `Logger::~Logger`) -- intentionally never
called due to `base::NoDestructor`.
- **WinHTTP internals** (e.g., `ScopedWinHttpHandle` default constructor,
`DownloadSink::OnError` base class method) -- trivial internal plumbing
exercised implicitly through download calls.
- **Interactive message-box backend** (`MessageBoxExW`) -- requires user
interaction. `ShowErrorDialog` and `ShowErrorMessageBox` are covered through
the test message-box runner.
- **Process creation** (e.g., `CopyFileIfExists` in installer_relaunch.cc)
-- requires real process spawning that cannot be mocked in unit tests.
- **DLL export function** (`RunInstaller`) -- thin C ABI wrapper around
`Controller::Run()`; tested indirectly through the controller.
- **Registry path validation** (`IsRegistryPathValid` in installer_paths.cc)
-- requires HKLM registry entries that unit tests cannot create without
admin privileges.
- **`TryLoadInstallerConfig` official-build branch** -- the
`enable_explicit_modes` command gate in official builds is unreachable in
non-official test builds. Resource provenance and bootstrap-only policy
parsing are covered by `ConfigPrecedenceTest`.
- **Unreachable switch fallthroughs** -- default cases in exhaustive switch
statements that exist for compiler warnings but can never execute.
@@ -0,0 +1,62 @@
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
if (is_win) {
# Mock client DLL for E2E launcher/RunInstaller tests.
shared_library("cef_e2e_mock_client") {
testonly = true
sources = [ "mock_client_dll/cef_mock_client.cc" ]
deps = [
"//cef:make_api_versions_header",
"//cef:make_config_header",
"//cef:make_version_header",
"//cef/libcef/features:buildflags",
]
libs = [ "kernel32.lib" ]
configs += [ "//cef/libcef/features:config" ]
}
# Helper binary that receives WM_COPYDATA progress notifications.
executable("cef_e2e_progress_helper") {
testonly = true
sources = [ "progress_helper/progress_helper.cc" ]
libs = [ "user32.lib" ]
}
# Helper tool that builds signed test distributions in CDN layout for E2E.
executable("cef_e2e_build_test_cdn") {
testonly = true
sources = [
"../installer_test_archive.cc",
"../installer_test_archive.h",
"../installer_test_catalog.cc",
"../installer_test_catalog.h",
"../installer_test_helpers.cc",
"../installer_test_helpers.h",
"build_test_cdn.cc",
]
data = [ "../testdata/test_signing.pfx" ]
deps = [
"//base",
"//cef/libcef_dll/bootstrap/installer:cef_installer",
]
configs += [
"//build/config:precompiled_headers",
"//cef/libcef/features:config",
]
}
group("e2e") {
testonly = true
deps = [
":cef_e2e_build_test_cdn",
":cef_e2e_mock_client",
":cef_e2e_progress_helper",
]
}
}
Whitespace-only changes.
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Build a local CDN directory for E2E tests.
Delegates to the C++ cef_e2e_build_test_cdn helper which wraps
test::BuildTestDistribution() for signing and archive creation.
When --version/--abi-hash are omitted, reads vmin and abi_hash from
gen/cef/installer_config.json in the build directory (derived from
cef_api_versions.json at build time) and constructs a default version
as <milestone>.1.
Usage:
# Auto-detect version from build config:
python3 build_local_cdn.py --build-dir=out/Debug_GN_x64 \
--output-dir=/tmp/cdn
# Explicit version:
python3 build_local_cdn.py --build-dir=out/Debug_GN_x64 \
--output-dir=/tmp/cdn --version=151.1 --abi-hash=a1b2c3d4e5f6
# Multi-version:
python3 build_local_cdn.py --build-dir=out/Debug_GN_x64 \
--output-dir=/tmp/cdn \
--version=151.1 --abi-hash=a1b2c3d4e5f6 \
--version=151.1.1 --abi-hash=a1b2c3d4e5f6
"""
import argparse
import json
import os
import subprocess
import sys
from e2e_config import get_test_versions
def build_cdn(build_dir, output_dir, versions):
"""Build a local CDN directory with the given versions.
Args:
build_dir: Path to the build directory containing cef_e2e_build_test_cdn.
output_dir: Directory to create CDN files in.
versions: List of (version, abi_hash) tuples.
Returns:
True on success.
"""
helper = os.path.join(build_dir, 'cef_e2e_build_test_cdn.exe')
if not os.path.isfile(helper):
print(f'Error: cef_e2e_build_test_cdn.exe not found at {helper}',
file=sys.stderr)
print('Build it with: autoninja -C <build_dir> cef_e2e_build_test_cdn',
file=sys.stderr)
return False
os.makedirs(output_dir, exist_ok=True)
for version, abi_hash in versions:
cmd = [
helper, f'--output-dir={output_dir}', f'--version={version}',
f'--abi-hash={abi_hash}'
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f'Error building CDN for {version}: {result.stderr}',
file=sys.stderr)
return False
# Write empty revoked.json if not present.
revoked_path = os.path.join(output_dir, 'revoked.json')
if not os.path.isfile(revoked_path):
with open(revoked_path, 'w') as f:
json.dump({'revoked_versions': []}, f)
return True
def main():
parser = argparse.ArgumentParser(
description='Build a local CDN directory for E2E tests')
parser.add_argument('--build-dir', required=True)
parser.add_argument('--output-dir', required=True)
parser.add_argument('--version', action='append')
parser.add_argument('--abi-hash', action='append')
args = parser.parse_args()
if args.version or args.abi_hash:
if not args.version or not args.abi_hash:
print('Error: --version and --abi-hash must both be provided',
file=sys.stderr)
return 1
if len(args.version) != len(args.abi_hash):
print('Error: --version and --abi-hash must be paired', file=sys.stderr)
return 1
versions = list(zip(args.version, args.abi_hash))
else:
result = get_test_versions(args.build_dir)
if result is None:
print(
'Error: could not read installer_config.json from '
f'{args.build_dir}/gen/cef/',
file=sys.stderr)
print('Build with: autoninja -C <build_dir> cef_installer_config',
file=sys.stderr)
return 1
version, _, _, abi_hash = result
versions = [(version, abi_hash)]
if build_cdn(args.build_dir, args.output_dir, versions):
return 0
return 1
if __name__ == '__main__':
sys.exit(main())
@@ -0,0 +1,118 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Helper tool that wraps test::BuildTestDistribution() to create a local CDN
// directory for E2E tests. Produces the same signed .tar.xz archives and
// manifests that the integration tests use, but writes them to disk in the
// CDN directory layout that the installer expects with /cef-download-path.
//
// Usage:
// cef_e2e_build_test_cdn --output-dir=C:\tmp\cdn --version=150.1
// --abi-hash=a1b2c3d4e5f6
#include <iostream>
#include "base/at_exit.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "cef/libcef_dll/bootstrap/installer/installer_download.h"
#include "cef/libcef_dll/bootstrap/installer/installer_test_archive.h"
#include "cef/libcef_dll/bootstrap/installer/installer_test_helpers.h"
namespace {
constexpr char kSwitchOutputDir[] = "output-dir";
constexpr char kSwitchVersion[] = "version";
constexpr char kSwitchAbiHash[] = "abi-hash";
} // namespace
int main(int argc, char* argv[]) {
base::AtExitManager at_exit;
base::CommandLine::Init(argc, argv);
const base::CommandLine& cmd = *base::CommandLine::ForCurrentProcess();
std::string output_dir_str = cmd.GetSwitchValueASCII(kSwitchOutputDir);
std::string version = cmd.GetSwitchValueASCII(kSwitchVersion);
std::string abi_hash = cmd.GetSwitchValueASCII(kSwitchAbiHash);
if (output_dir_str.empty() || version.empty()) {
std::cerr << "Usage: cef_e2e_build_test_cdn --output-dir=<path> "
<< "--version=<ver> --abi-hash=<hash>" << std::endl;
return 1;
}
if (abi_hash.empty()) {
abi_hash = "a1b2c3d4e5f6";
}
base::FilePath output_dir = base::FilePath::FromUTF8Unsafe(output_dir_str);
base::CreateDirectory(output_dir);
base::FilePath pfx_path =
cef_installer::test::GetTestDataPath().AppendASCII("test_signing.pfx");
auto dist = cef_installer::test::BuildTestDistribution(
version, abi_hash, output_dir, pfx_path, "test");
if (!dist.has_value()) {
std::cerr << "Failed to build test distribution for " << version
<< std::endl;
return 2;
}
// Rename archive to match CDN naming convention.
std::string cdn_filename =
"cef_" + version + "_" + dist->platform + ".tar.xz";
base::FilePath cdn_archive = output_dir.AppendASCII(cdn_filename);
if (cdn_archive != dist->archive_path) {
base::Move(dist->archive_path, cdn_archive);
}
// Write SHA256 sidecar.
std::string sha256 = cef_installer::ComputeFileSha256(cdn_archive);
base::FilePath sha256_path = output_dir.AppendASCII(cdn_filename + ".sha256");
base::WriteFile(sha256_path, sha256);
// Write manifest JSON.
std::string manifest =
R"([{"version": ")" + version + R"(", "file": ")" + cdn_filename +
R"(", "sha1": ")" + dist->archive_sha1 +
R"(", "last_modified": "2026-01-01T00:00:00Z", "abi_hash": ")" +
abi_hash + R"("}])";
// Write manifest under both naming conventions:
// - milestone-based: 150_windows64.json (used when no abi_hash in config)
// - abi_hash-based: a1b2c3d4e5f6_windows64.json (used when abi_hash set)
std::string milestone = version.substr(0, version.find('.'));
std::vector<std::string> manifest_names = {
milestone + "_" + dist->platform + ".json",
abi_hash + "_" + dist->platform + ".json",
};
for (const auto& name : manifest_names) {
base::FilePath manifest_path = output_dir.AppendASCII(name);
// If manifest already exists, merge (for multi-version).
std::string existing;
if (base::ReadFileToString(manifest_path, &existing) &&
existing.size() > 2) {
std::string merged = existing;
merged.pop_back(); // Remove trailing ]
merged += "," + manifest.substr(1); // Append new entry (skip leading [)
base::WriteFile(manifest_path, merged);
} else {
base::WriteFile(manifest_path, manifest);
}
}
// Write empty revoked.json if not present.
base::FilePath revoked_path = output_dir.AppendASCII("revoked.json");
if (!base::PathExists(revoked_path)) {
base::WriteFile(revoked_path, R"({"revoked_versions": []})");
}
std::cout << "Built CDN distribution: " << cdn_filename << std::endl;
return 0;
}
@@ -0,0 +1,109 @@
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
if (is_win) {
# Test DLLs with embedded CEF_INSTALLER_CONFIG resources for config
# precedence unit tests.
shared_library("cef_config_test_appid_a") {
testonly = true
sources = [
"config_appid_a.rc",
"dll_stub.cc",
]
}
shared_library("cef_config_test_appid_b") {
testonly = true
sources = [
"config_appid_b.rc",
"dll_stub.cc",
]
}
shared_library("cef_config_test_explicit_modes") {
testonly = true
sources = [
"config_explicit_modes.rc",
"dll_stub.cc",
]
}
shared_library("cef_config_test_cdn_wrong_shape") {
testonly = true
sources = [
"config_cdn_wrong_shape.rc",
"dll_stub.cc",
]
}
shared_library("cef_config_test_cdn_empty") {
testonly = true
sources = [
"config_cdn_empty.rc",
"dll_stub.cc",
]
}
shared_library("cef_config_test_cdn_overcount") {
testonly = true
sources = [
"config_cdn_overcount.rc",
"dll_stub.cc",
]
}
shared_library("cef_config_test_cdn_overlength") {
testonly = true
sources = [
"config_cdn_overlength.rc",
"dll_stub.cc",
]
}
shared_library("cef_config_test_no_config") {
testonly = true
sources = [ "dll_stub.cc" ]
}
shared_library("cef_config_test_malformed") {
testonly = true
sources = [
"config_malformed.rc",
"dll_stub.cc",
]
}
shared_library("cef_config_test_invalid") {
testonly = true
sources = [
"config_invalid.rc",
"dll_stub.cc",
]
}
shared_library("cef_config_test_invalid_cdn") {
testonly = true
sources = [
"config_invalid_cdn.rc",
"dll_stub.cc",
]
}
group("config_test_dlls") {
testonly = true
deps = [
":cef_config_test_appid_a",
":cef_config_test_appid_b",
":cef_config_test_cdn_empty",
":cef_config_test_cdn_overcount",
":cef_config_test_cdn_overlength",
":cef_config_test_cdn_wrong_shape",
":cef_config_test_explicit_modes",
":cef_config_test_invalid",
":cef_config_test_invalid_cdn",
":cef_config_test_malformed",
":cef_config_test_no_config",
]
}
}
@@ -0,0 +1 @@
{"appid":"aaaaaaaa-1111-1111-1111-111111111111","vmin":"1.0","install_path":"relative_store","cdn_urls":["https://client.example/prefix","https://client.example/prefix"]}
@@ -0,0 +1 @@
CEF_INSTALLER_CONFIG RCDATA "cef/libcef_dll/bootstrap/installer/e2e/config_test_dlls/config_appid_a.json"
@@ -0,0 +1 @@
{"appid":"bbbbbbbb-2222-2222-2222-222222222222","vmin":"1.0","cdn_urls":["https://bootstrap.example/"]}
@@ -0,0 +1 @@
CEF_INSTALLER_CONFIG RCDATA "cef/libcef_dll/bootstrap/installer/e2e/config_test_dlls/config_appid_b.json"
@@ -0,0 +1 @@
{"appid":"f2000000-6666-6666-6666-666666666666","vmin":"1.0","cdn_urls":[]}
@@ -0,0 +1 @@
CEF_INSTALLER_CONFIG RCDATA "cef/libcef_dll/bootstrap/installer/e2e/config_test_dlls/config_cdn_empty.json"
@@ -0,0 +1 @@
{"appid":"f3000000-6666-6666-6666-666666666666","vmin":"1.0","cdn_urls":["https://1.example/","https://2.example/","https://3.example/","https://4.example/"]}
@@ -0,0 +1 @@
CEF_INSTALLER_CONFIG RCDATA "cef/libcef_dll/bootstrap/installer/e2e/config_test_dlls/config_cdn_overcount.json"
@@ -0,0 +1 @@
{"appid":"f4000000-6666-6666-6666-666666666666","vmin":"1.0","cdn_urls":["https://example.com/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]}
@@ -0,0 +1 @@
CEF_INSTALLER_CONFIG RCDATA "cef/libcef_dll/bootstrap/installer/e2e/config_test_dlls/config_cdn_overlength.json"
@@ -0,0 +1 @@
{"appid":"f1000000-6666-6666-6666-666666666666","vmin":"1.0","cdn_urls":"https://one.example/"}
@@ -0,0 +1 @@
CEF_INSTALLER_CONFIG RCDATA "cef/libcef_dll/bootstrap/installer/e2e/config_test_dlls/config_cdn_wrong_shape.json"
@@ -0,0 +1 @@
{"appid":"eeeeeeee-5555-5555-5555-555555555555","vmin":"1.0","enable_explicit_modes":true}
@@ -0,0 +1 @@
CEF_INSTALLER_CONFIG RCDATA "config_explicit_modes.json"
@@ -0,0 +1 @@
{"appid":"eeeeeeee-5555-5555-5555-555555555555"}
@@ -0,0 +1 @@
CEF_INSTALLER_CONFIG RCDATA "cef/libcef_dll/bootstrap/installer/e2e/config_test_dlls/config_invalid.json"
@@ -0,0 +1 @@
{"appid":"ffffffff-6666-6666-6666-666666666666","vmin":"1.0","cdn_urls":["http://insecure.example/"]}
@@ -0,0 +1 @@
CEF_INSTALLER_CONFIG RCDATA "cef/libcef_dll/bootstrap/installer/e2e/config_test_dlls/config_invalid_cdn.json"
@@ -0,0 +1 @@
{"appid":
@@ -0,0 +1 @@
CEF_INSTALLER_CONFIG RCDATA "cef/libcef_dll/bootstrap/installer/e2e/config_test_dlls/config_malformed.json"
@@ -0,0 +1,11 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <windows.h>
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved) {
return TRUE;
}
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Shared helpers for reading build-time installer configuration."""
import json
import os
def read_installer_config(build_dir):
"""Read gen/cef/installer_config.json from the build directory.
Returns the parsed dict, or None if the file is missing.
"""
config_path = os.path.join(build_dir, 'gen', 'cef', 'installer_config.json')
if not os.path.isfile(config_path):
return None
with open(config_path) as f:
return json.load(f)
def get_test_versions(build_dir):
"""Derive test version strings from the build's installer config.
Returns (test_version, test_version_update, test_version_higher, abi_hash)
or None if the config cannot be read.
The versions are constructed from the milestone in vmin so they stay
in sync when the API version changes:
test_version: <milestone>.1
test_version_update: <milestone>.1.1
test_version_higher: <milestone>.2
"""
config = read_installer_config(build_dir)
if config is None:
return None
vmin = config.get('vmin', '')
if not vmin:
return None
milestone = vmin.split('.')[0]
abi_hash = config.get('abi_hash', '')
return (f'{milestone}.1', f'{milestone}.1.1', f'{milestone}.2', abi_hash)
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Test-selection helpers for the E2E runner.
`compile_filter` turns a pytest-style -k expression into a predicate over a
test id; `flatten_suite` yields individual test cases from a nested suite.
Kept separate from run_e2e_tests.py so it is unit-testable without a build.
"""
import ast
import unittest
def _is_boolean_expr(node):
"""True if `node` is built only from and/or/not over bare keywords.
Anything else -- a binary op (`a-b`, `a+b`), attribute access (`mod.Class`),
a call, a comparison, a literal -- is not a keyword expression we evaluate;
the caller treats the whole string as a literal substring instead.
"""
if isinstance(node, ast.Expression):
return _is_boolean_expr(node.body)
if isinstance(node, ast.BoolOp):
return all(_is_boolean_expr(v) for v in node.values)
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
return _is_boolean_expr(node.operand)
return isinstance(node, ast.Name)
def compile_filter(expr):
"""Compile a pytest-style -k expression into predicate(test_id) -> bool.
Supports `and`, `or`, `not`, parentheses, and bare keywords matched
case-insensitively as substrings of the test id. Any expression that is not
a pure boolean combination of bare keywords -- because it fails to parse
(`a or`) or uses non-keyword syntax (`a-b`, `mod.Class.test_x`, `a + b`) --
falls back to plain substring containment of the whole string, so any
single-keyword usage works regardless of the characters it contains.
"""
try:
tree = ast.parse(expr, mode='eval')
except SyntaxError:
tree = None
if tree is None or not _is_boolean_expr(tree):
return lambda test_id: expr.lower() in test_id.lower()
def evaluate(node, test_id):
if isinstance(node, ast.Expression):
return evaluate(node.body, test_id)
if isinstance(node, ast.BoolOp):
results = [evaluate(v, test_id) for v in node.values]
return all(results) if isinstance(node.op, ast.And) else any(results)
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
return not evaluate(node.operand, test_id)
# _is_boolean_expr guarantees this is an ast.Name.
return node.id.lower() in test_id.lower()
return lambda test_id: evaluate(tree, test_id)
def flatten_suite(suite):
"""Yield individual test cases from an arbitrarily nested TestSuite."""
for item in suite:
if isinstance(item, unittest.TestSuite):
yield from flatten_suite(item)
else:
yield item
@@ -0,0 +1,819 @@
#!/usr/bin/env python3
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Base class for CEF installer E2E tests."""
import contextlib
import ctypes
from ctypes import wintypes
import json
import http.server
import os
import shutil
import ssl
import struct
import subprocess
import tempfile
import threading
import time
import unittest
import zlib
from e2e_config import get_test_versions
# Resolved from CEF_E2E_BUILD_DIR environment variable.
BUILD_DIR = None
# Path to testdata/ directory containing certificates.
TESTDATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..',
'testdata')
_INTEGRITY_FOOTER_MAGIC = 0xCEF09E1F4A3B2C1D
# Command-line flags (must match installer_bootstrap_helpers.h).
FLAG_UPDATE = '/cef-update'
FLAG_UNINSTALL = '/cef-uninstall'
FLAG_UNINSTALL_RELAUNCHED = '/cef-uninstall-relaunched'
FLAG_RETENTION_DRY_RUN = '/cef-retention-dry-run'
FLAG_RETENTION_APPLY = '/cef-retention-apply'
FLAG_RETENTION_MAX_AGE_DAYS = '/cef-max-age-days'
FLAG_HEADLESS = '/cef-headless'
FLAG_FORCECHECK = '/cef-forcecheck'
FLAG_BACKGROUND = '/cef-background'
FLAG_PARENT = '/cef-parent'
FLAG_INSTALL_PATH = '--cef-install-path'
FLAG_DOWNLOAD_PATH = '--cef-download-path'
# Exit codes (must match installer_constants.h).
EXIT_SUCCESS = 0
EXIT_CONFIG_ERROR = 100
EXIT_NETWORK_ERROR = 101
EXIT_SIGNATURE_ERROR = 102
EXIT_NO_MATCHING_VERSION = 103
EXIT_EXTRACTION_ERROR = 104
EXIT_INSTALL_ERROR = 105
EXIT_DATABASE_ERROR = 106
EXIT_LOCK_TIMEOUT = 107
EXIT_CANCELLED = 108
EXIT_RELAUNCHED = 109
EXIT_NO_SENTINEL = 110
EXIT_SENTINEL_READ_ERROR = 111
EXIT_SENTINEL_OWNER_MISMATCH = 112
EXIT_POLICY_DENIED = 113
EXIT_INDEX_ERROR = 114
EXIT_POLICY_ERROR = 119
class _TrusteeW(ctypes.Structure):
_fields_ = [
('multiple_trustee', ctypes.c_void_p),
('multiple_trustee_operation', wintypes.DWORD),
('trustee_form', wintypes.DWORD),
('trustee_type', wintypes.DWORD),
('name', ctypes.c_void_p),
]
class _ExplicitAccessW(ctypes.Structure):
_fields_ = [
('access_permissions', wintypes.DWORD),
('access_mode', wintypes.DWORD),
('inheritance', wintypes.DWORD),
('trustee', _TrusteeW),
]
class _ScopedDirectoryWriteDeny:
"""Temporarily deny directory-entry creation while preserving DACL repair."""
_SE_FILE_OBJECT = 1
_DACL_SECURITY_INFORMATION = 0x00000004
_WIN_WORLD_SID = 1
_SECURITY_MAX_SID_SIZE = 68
_DENY_ACCESS = 3
_TRUSTEE_IS_SID = 0
_TRUSTEE_IS_WELL_KNOWN_GROUP = 5
# FILE_ADD_FILE | FILE_ADD_SUBDIRECTORY | FILE_WRITE_EA |
# FILE_WRITE_ATTRIBUTES. WRITE_DAC and deletion remain available so cleanup
# can always restore the original descriptor and remove the fixture.
_DIRECTORY_WRITE_MASK = 0x00000116
def __init__(self, path):
self._path = os.path.abspath(path)
self._security_descriptor = ctypes.c_void_p()
self._original_dacl = ctypes.c_void_p()
self._new_dacl = ctypes.c_void_p()
self._applied = False
self._advapi32 = ctypes.WinDLL('advapi32', use_last_error=True)
self._kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
self._configure_functions()
def _configure_functions(self):
self._advapi32.GetNamedSecurityInfoW.argtypes = [
wintypes.LPWSTR, wintypes.DWORD, wintypes.DWORD,
ctypes.POINTER(ctypes.c_void_p),
ctypes.POINTER(ctypes.c_void_p),
ctypes.POINTER(ctypes.c_void_p),
ctypes.POINTER(ctypes.c_void_p),
ctypes.POINTER(ctypes.c_void_p)
]
self._advapi32.GetNamedSecurityInfoW.restype = wintypes.DWORD
self._advapi32.CreateWellKnownSid.argtypes = [
wintypes.DWORD, ctypes.c_void_p, ctypes.c_void_p,
ctypes.POINTER(wintypes.DWORD)
]
self._advapi32.CreateWellKnownSid.restype = wintypes.BOOL
self._advapi32.SetEntriesInAclW.argtypes = [
wintypes.ULONG,
ctypes.POINTER(_ExplicitAccessW), ctypes.c_void_p,
ctypes.POINTER(ctypes.c_void_p)
]
self._advapi32.SetEntriesInAclW.restype = wintypes.DWORD
self._advapi32.SetNamedSecurityInfoW.argtypes = [
wintypes.LPWSTR, wintypes.DWORD, wintypes.DWORD, ctypes.c_void_p,
ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p
]
self._advapi32.SetNamedSecurityInfoW.restype = wintypes.DWORD
self._kernel32.LocalFree.argtypes = [ctypes.c_void_p]
self._kernel32.LocalFree.restype = ctypes.c_void_p
@staticmethod
def _raise_security_error(result, operation):
raise OSError(result, f'{operation} failed with Windows error {result}')
def apply(self):
path_buffer = ctypes.create_unicode_buffer(self._path)
result = self._advapi32.GetNamedSecurityInfoW(
path_buffer, self._SE_FILE_OBJECT, self._DACL_SECURITY_INFORMATION,
None, None, ctypes.byref(self._original_dacl), None,
ctypes.byref(self._security_descriptor))
if result:
self._raise_security_error(result, 'GetNamedSecurityInfoW')
sid_size = wintypes.DWORD(self._SECURITY_MAX_SID_SIZE)
world_sid = ctypes.create_string_buffer(sid_size.value)
if not self._advapi32.CreateWellKnownSid(self._WIN_WORLD_SID, None,
world_sid, ctypes.byref(sid_size)):
error = ctypes.get_last_error()
self._release()
raise ctypes.WinError(error)
access = _ExplicitAccessW()
access.access_permissions = self._DIRECTORY_WRITE_MASK
access.access_mode = self._DENY_ACCESS
access.inheritance = 0
access.trustee.multiple_trustee = None
access.trustee.multiple_trustee_operation = 0
access.trustee.trustee_form = self._TRUSTEE_IS_SID
access.trustee.trustee_type = self._TRUSTEE_IS_WELL_KNOWN_GROUP
access.trustee.name = ctypes.cast(world_sid, ctypes.c_void_p)
result = self._advapi32.SetEntriesInAclW(1, ctypes.byref(access),
self._original_dacl,
ctypes.byref(self._new_dacl))
if result:
self._release()
self._raise_security_error(result, 'SetEntriesInAclW')
result = self._advapi32.SetNamedSecurityInfoW(
path_buffer, self._SE_FILE_OBJECT, self._DACL_SECURITY_INFORMATION,
None, None, self._new_dacl, None)
if result:
self._release()
self._raise_security_error(result, 'SetNamedSecurityInfoW')
self._applied = True
def restore(self):
if not self._applied:
return
path_buffer = ctypes.create_unicode_buffer(self._path)
result = self._advapi32.SetNamedSecurityInfoW(
path_buffer, self._SE_FILE_OBJECT, self._DACL_SECURITY_INFORMATION,
None, None, self._original_dacl, None)
if result:
self._raise_security_error(result, 'DACL restoration')
self._applied = False
self._release()
def _release(self):
if self._new_dacl.value:
self._kernel32.LocalFree(self._new_dacl)
self._new_dacl = ctypes.c_void_p()
if self._security_descriptor.value:
self._kernel32.LocalFree(self._security_descriptor)
self._security_descriptor = ctypes.c_void_p()
def _get_build_dir():
global BUILD_DIR
if BUILD_DIR is None:
BUILD_DIR = os.environ.get('CEF_E2E_BUILD_DIR', '')
return BUILD_DIR
class E2ETestBase(unittest.TestCase):
"""Base class for CEF installer end-to-end tests.
The exe lives in the build directory (component build requires all DLLs
to be co-located). We create CefTestApp.exe as a copy of bootstrap.exe
inside the build dir and embed each test's application config in that
private copy. Test isolation comes from --cef-install-path and
--cef-download-path pointing to per-test temp dirs.
"""
def tearDown(self):
# Print installer log on test failure to aid debugging.
if hasattr(self, '_outcome'):
# Python 3.4+
result = self._outcome.result
errors = (result.failures + result.errors) if result else []
if any(test is self for test, _ in errors):
log_path = os.path.join(self.install_dir, 'cef_installer.log')
if os.path.isfile(log_path):
with open(log_path) as f:
print(f'\n--- cef_installer.log ---\n{f.read()}---')
@contextlib.contextmanager
def scoped_read_only_directory(self, path):
"""Deny write probes for an owned temp directory, then restore its DACL."""
fixture = _ScopedDirectoryWriteDeny(path)
try:
fixture.apply()
except OSError as error:
self.skipTest(f'NTFS DACL fixture is unavailable: {error}')
try:
yield
finally:
fixture.restore()
def setUp(self):
build_dir = _get_build_dir()
if not build_dir or not os.path.isdir(build_dir):
self.skipTest(f'Build directory not found: {build_dir}')
self.build_dir = build_dir
self.temp_dir = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, self.temp_dir, ignore_errors=True)
self.install_dir = os.path.join(self.temp_dir, 'CEF')
self.cdn_dir = os.path.join(self.temp_dir, 'cdn')
# Derive test versions from the build's installer_config.json so they
# stay in sync when the API version changes.
versions = get_test_versions(build_dir)
if versions is None:
self.skipTest(f'installer_config.json not found in {build_dir}/gen/cef/')
(self.test_version, self.test_version_update, self.test_version_higher,
_) = versions
# The exe must live in the build directory so it can find component
# DLLs. Restore bootstrap.exe -> CefTestApp.exe for every test before any
# resource edits.
# The bootstrap fatals if the exe name is "bootstrap" (expects to be
# renamed).
src_exe = os.path.join(build_dir, 'bootstrap.exe')
if not os.path.isfile(src_exe):
self.skipTest(f'bootstrap.exe not found at {src_exe}')
self.app_name = 'CefTestApp'
self.bootstrap_exe = os.path.join(build_dir, f'{self.app_name}.exe')
self._restore_bootstrap_executable(self.bootstrap_exe)
self.mock_client_dll_src = os.path.join(build_dir,
'cef_e2e_mock_client.dll')
# Read the test certificate thumbprint.
thumbprint_path = os.path.join(TESTDATA_DIR, 'test_thumbprint.txt')
if os.path.isfile(thumbprint_path):
with open(thumbprint_path) as f:
self.test_thumbprint = f.read().strip()
else:
self.test_thumbprint = ''
def _restore_bootstrap_executable(self, destination):
"""Unconditionally restore an unmodified bootstrap executable."""
source = os.path.join(self.build_dir, 'bootstrap.exe')
try:
shutil.copy2(source, destination)
except PermissionError as error:
self.fail('Cannot restore bootstrap fixture while a prior test process '
f'is using it: {destination}: {error}')
def setup_launcher(self, app_name='TestApp'):
"""Copy bootstrap.exe and mock client DLL with matching names.
The bootstrap derives the DLL name from the exe name
(GetDefaultModuleValue), so TestApp.exe loads TestApp.dll.
Both must be in the build directory for component builds.
"""
app_exe = os.path.join(self.build_dir, f'{app_name}.exe')
app_dll = os.path.join(self.build_dir, f'{app_name}.dll')
self._restore_bootstrap_executable(app_exe)
if os.path.isfile(self.mock_client_dll_src):
shutil.copy2(self.mock_client_dll_src, app_dll)
else:
self.skipTest('cef_e2e_mock_client.dll not found')
# Clean up launcher files after test.
self.addCleanup(lambda: os.remove(app_dll)
if os.path.isfile(app_dll) else None)
self.addCleanup(lambda: os.remove(app_exe)
if os.path.isfile(app_exe) else None)
return app_exe, app_name
def embed_test_config(self,
appid,
vmin,
abi_hash='',
vmax='',
exe=None,
**extra):
"""Embed a complete runtime config in an already restored executable."""
config = {
'appid': appid,
'vmin': vmin,
'abi_hash': abi_hash,
'enable_explicit_modes': True,
}
if self.test_thumbprint:
config['certificate_thumbprint'] = self.test_thumbprint
if vmax:
config['vmax'] = vmax
config.update(extra)
target = exe or self.bootstrap_exe
self.embed_config_resource(target, config)
return target
def run_installer(self,
args=None,
use_local_cdn=True,
allow_configured_cdn=False,
install_path=None,
use_install_path_override=True,
timeout=30,
exe=None,
env=None):
"""Run the exe with args, return (exit_code, stdout, stderr)."""
cmd = [exe or self.bootstrap_exe] + (args or [])
# The bootstrap resolves CEF before loading a client DLL, which may
# show a progress/error dialog. Ensure /cef-headless is always present
# so E2E tests don't block on UI.
if FLAG_HEADLESS not in cmd:
cmd.append(FLAG_HEADLESS)
# Pass --cef-download-path to prevent accidental real-CDN access. The one
# explicit exception is allow_configured_cdn, used only with loopback HTTPS
# URLs by the application-source process test below.
if allow_configured_cdn:
pass
elif use_local_cdn and os.path.isdir(self.cdn_dir):
cmd.append(f'{FLAG_DOWNLOAD_PATH}={self.cdn_dir}')
elif not any(FLAG_DOWNLOAD_PATH in a for a in cmd):
os.makedirs(self.cdn_dir, exist_ok=True)
cmd.append(f'{FLAG_DOWNLOAD_PATH}={self.cdn_dir}')
if use_install_path_override:
path = install_path or self.install_dir
cmd.append(f'{FLAG_INSTALL_PATH}={path}')
run_env = os.environ.copy()
if env:
run_env.update(env)
proc = subprocess.run(cmd,
capture_output=True,
timeout=timeout,
cwd=self.build_dir,
env=run_env)
return proc.returncode, proc.stdout, proc.stderr
def stage_bootstrap_in_install_root(self,
install_root,
app_name='ContainedCefTestApp'):
"""Stage a runnable component-build bootstrap beneath an install root."""
os.makedirs(install_root, exist_ok=True)
staged_exe = os.path.join(install_root, f'{app_name}.exe')
shutil.copy2(self.bootstrap_exe, staged_exe)
shutil.copy2(os.path.join(self.build_dir, 'chrome_elf.dll'),
os.path.join(install_root, 'chrome_elf.dll'))
crash_config = os.path.join(self.build_dir, 'crash_reporter.cfg')
if os.path.isfile(crash_config):
shutil.copy2(crash_config, os.path.join(install_root,
'crash_reporter.cfg'))
return staged_exe
@staticmethod
def snapshot_uninstall_relaunch_dirs():
"""Return the existing installer-owned relaunch directories."""
temp_root = tempfile.gettempdir()
try:
entries = os.scandir(temp_root)
except OSError:
return set()
with entries:
return {
os.path.normcase(os.path.abspath(entry.path))
for entry in entries
if entry.is_dir(
follow_symlinks=False) and entry.name.startswith('cef_uninstall_')
}
@staticmethod
def _database_contains_app(install_root, appid):
db_path = os.path.join(install_root, 'installer.json')
if not os.path.isfile(db_path):
return False
with open(db_path, 'rb') as db_file:
text = db_file.read().decode('utf-8', errors='replace')
start = text.find('{')
if start < 0:
return False
try:
value, _ = json.JSONDecoder().raw_decode(text, start)
except json.JSONDecodeError:
return True
return any(app.get('uuid') == appid for app in value.get('apps', []))
@staticmethod
def _read_version_index_versions(install_root):
"""Return the integrity-checked version/platform keys, or None."""
index_path = os.path.join(install_root, 'versions.json')
try:
with open(index_path, 'rb') as index_file:
raw = index_file.read()
except OSError:
return None
if len(raw) < 16:
return None
content = raw[:-16]
expected_crc, reserved, magic = struct.unpack('<IIQ', raw[-16:])
if (reserved != 0 or magic != _INTEGRITY_FOOTER_MAGIC or
zlib.crc32(content) & 0xffffffff != expected_crc):
return None
try:
value = json.loads(content.decode('utf-8'))
except (UnicodeDecodeError, json.JSONDecodeError):
return None
versions = value.get('versions') if isinstance(value, dict) else None
if not isinstance(versions, list):
return None
keys = set()
for entry in versions:
if not isinstance(entry, dict):
return None
version = entry.get('version')
platform = entry.get('platform')
if not isinstance(version, str) or not isinstance(platform, str):
return None
key = (version, platform)
if key in keys:
return None
keys.add(key)
return keys
def wait_for_relaunched_uninstall(self,
appid,
previous_dirs,
expected_exe_name,
expected_index_versions,
install_root=None,
timeout=15):
"""Wait for a child to publish the expected commit and exit, then clean."""
install_root = install_root or self.install_dir
expected_index_versions = set(expected_index_versions)
deadline = time.time() + timeout
relaunch_dir = None
while time.time() < deadline and relaunch_dir is None:
candidates = self.snapshot_uninstall_relaunch_dirs() - previous_dirs
matching = [
path for path in candidates
if os.path.isfile(os.path.join(path, expected_exe_name))
]
if len(matching) == 1:
relaunch_dir = matching[0]
self.addCleanup(shutil.rmtree, relaunch_dir, ignore_errors=True)
break
time.sleep(0.1)
self.assertIsNotNone(relaunch_dir,
f'Expected one relaunched {expected_exe_name} child')
registration_removed = False
index_versions = None
while time.time() < deadline:
registration_removed = not self._database_contains_app(
install_root, appid)
index_versions = self._read_version_index_versions(install_root)
if (registration_removed and index_versions == expected_index_versions):
try:
shutil.rmtree(relaunch_dir)
return relaunch_dir
except OSError:
pass
time.sleep(0.1)
self.fail('Relaunched uninstall did not publish its complete logical '
f'commit and exit before timeout: registration_removed='
f'{registration_removed}, index_versions={index_versions}')
def get_loopback_tls_certificate(self):
"""Return the checked-in, test-only localhost TLS certificate and key."""
cert_path = os.path.join(TESTDATA_DIR, 'loopback_tls_cert.pem')
key_path = os.path.join(TESTDATA_DIR, 'loopback_tls_key.pem')
self.assertTrue(os.path.isfile(cert_path),
f'Loopback TLS certificate not found: {cert_path}')
self.assertTrue(os.path.isfile(key_path),
f'Loopback TLS key not found: {key_path}')
return cert_path, key_path
def start_https_file_server(self, directory, cert_path, key_path):
"""Serve a directory over loopback HTTPS and record request paths."""
requests = []
class RecordingHandler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=directory, **kwargs)
def do_GET(self):
requests.append(f'GET {self.path}')
super().do_GET()
def do_HEAD(self):
requests.append(f'HEAD {self.path}')
super().do_HEAD()
def log_message(self, _format, *_args):
pass
server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), RecordingHandler)
server.daemon_threads = True
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(cert_path, key_path)
server.socket = context.wrap_socket(server.socket, server_side=True)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
def stop_server():
server.shutdown()
server.server_close()
thread.join(timeout=5)
self.addCleanup(stop_server)
return f'https://localhost:{server.server_port}/', requests
def assert_version_installed(self, version, platform='windows64'):
"""Assert that a version directory exists with valid metadata."""
version_dir = os.path.join(self.install_dir, 'Versions', version, platform)
self.assertTrue(os.path.isdir(version_dir),
f'Version directory not found: {version_dir}')
metadata_path = os.path.join(version_dir, 'cef_version.json')
self.assertTrue(os.path.isfile(metadata_path),
f'Metadata not found: {metadata_path}')
def assert_version_not_installed(self, version, platform='windows64'):
"""Assert that a version directory does not exist."""
version_dir = os.path.join(self.install_dir, 'Versions', version, platform)
self.assertFalse(os.path.exists(version_dir),
f'Version directory still exists: {version_dir}')
def build_cdn(self, version=None, abi_hash='a1b2c3d4e5f6'):
"""Build a local CDN directory with the given version.
Can be called multiple times to add versions (manifests merge).
"""
if version is None:
version = self.test_version
os.makedirs(self.cdn_dir, exist_ok=True)
helper = os.path.join(self.build_dir, 'cef_e2e_build_test_cdn.exe')
if not os.path.isfile(helper):
self.skipTest('cef_e2e_build_test_cdn.exe not found')
result = subprocess.run([
helper, f'--output-dir={self.cdn_dir}', f'--version={version}',
f'--abi-hash={abi_hash}'
],
capture_output=True,
encoding='utf-8',
errors='replace')
self.assertEqual(0, result.returncode, f'CDN build failed: {result.stderr}')
return version, abi_hash
def embed_config_resource(self, exe_path, config_dict):
"""Embed a CEF_INSTALLER_CONFIG resource into an exe using ResourceHacker.
Writes config JSON + .rc file to self.temp_dir, compiles with
ResourceHacker, then replaces/adds the resource in the target exe.
Fails the test (not skips) if CEF_RESOURCE_HACKER_PATH is not set.
"""
rh_path = os.environ.get('CEF_RESOURCE_HACKER_PATH', '')
if not rh_path or not os.path.isfile(rh_path):
self.fail('CEF_RESOURCE_HACKER_PATH must be set to the path of '
'ResourceHacker.exe (required for embedded config tests). '
f'Current value: {rh_path!r}')
self.embed_raw_config_resource(exe_path, json.dumps(config_dict))
def embed_raw_config_resource(self, exe_path, config_text):
"""Embed raw CEF_INSTALLER_CONFIG text for malformed-config tests."""
rh_path = os.environ.get('CEF_RESOURCE_HACKER_PATH', '')
if not rh_path or not os.path.isfile(rh_path):
self.fail('CEF_RESOURCE_HACKER_PATH must be set to the path of '
'ResourceHacker.exe (required for embedded config tests). '
f'Current value: {rh_path!r}')
config_json_path = os.path.join(self.temp_dir, 'config.json')
with open(config_json_path, 'w') as f:
f.write(config_text)
rc_path = os.path.join(self.temp_dir, 'config.rc')
with open(rc_path, 'w') as f:
f.write('CEF_INSTALLER_CONFIG RCDATA "config.json"\n')
res_path = os.path.join(self.temp_dir, 'config.res')
# Compile .rc -> .res (cwd must be temp_dir so relative "config.json" resolves).
result = subprocess.run([
rh_path, '-open', 'config.rc', '-save', 'config.res', '-action',
'compile'
],
capture_output=True,
encoding='utf-8',
errors='replace',
cwd=self.temp_dir)
self.assertEqual(0, result.returncode,
f'ResourceHacker compile failed: {result.stderr}')
# Add/overwrite the resource in the exe.
result = subprocess.run([
rh_path, '-open', exe_path, '-save', exe_path, '-action',
'addoverwrite', '-res', res_path, '-mask',
'RCDATA,CEF_INSTALLER_CONFIG,'
],
capture_output=True,
encoding='utf-8',
errors='replace',
cwd=self.temp_dir)
self.assertEqual(0, result.returncode,
f'ResourceHacker addoverwrite failed: {result.stderr}')
def get_appid_hash(self, appid):
"""Compute SHA-1 of appid, return first 16 hex chars.
Must match C++ GetAppidHash logic.
"""
import hashlib
h = hashlib.sha1(appid.encode('utf-8')).hexdigest()
return h[:16].upper()
def get_launch_dir(self):
"""Return path to the .launch/ directory under install_dir."""
return os.path.join(self.install_dir, '.launch')
@staticmethod
def filetime_now():
"""Return current wall time as Windows FILETIME ticks."""
return int((time.time() + 11644473600) * 10000000)
@staticmethod
def write_integrity_json(path, value):
"""Write compact JSON with the installer's CRC32 integrity footer."""
content = json.dumps(value, separators=(',', ':')).encode('utf-8')
footer = struct.pack('<IIQ',
zlib.crc32(content) & 0xffffffff, 0,
_INTEGRITY_FOOTER_MAGIC)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'wb') as f:
f.write(content)
f.write(footer)
def write_registration_database(self, apps):
"""Write a valid schema-1 installer.json with supplied app entries."""
path = os.path.join(self.install_dir, 'installer.json')
self.write_integrity_json(path, {
'schema_version': 1,
'apps': apps,
})
return path
def write_liveness(self,
appid,
last_launch,
platform='windows64',
filename=None):
"""Write an integrity-valid liveness-only record."""
appid_hash = self.get_appid_hash(appid)
name = filename or f'{appid_hash}_{platform}'
path = os.path.join(self.get_launch_dir(), name)
self.write_integrity_json(path, {
'appid': appid,
'platform': platform,
'last_launch': str(last_launch),
})
return path
def write_health(self,
appid,
version,
pid_start_time,
platform='windows64',
filename=None,
last_update=None,
running=False,
confirmed=True,
consecutive_failures=0):
"""Write an integrity-valid health sentinel with controlled FILETIME."""
if last_update is None:
last_update = self.filetime_now()
appid_hash = self.get_appid_hash(appid)
name = filename or f'{appid_hash}_{version}_{platform}'
path = os.path.join(self.get_launch_dir(), name)
self.write_integrity_json(
path, {
'appid': appid,
'pid': 0,
'pid_start_time': str(pid_start_time),
'consecutive_failures': consecutive_failures,
'running': running,
'confirmed': confirmed,
'version': version,
'platform': platform,
'last_update': str(last_update),
})
return path
def read_launch_state(self, version, appid_hash, platform='windows64'):
"""Read launch state from .launch/ directory.
Filename: <appid_hash>_<version>_<platform>
Returns dict or None.
"""
launch_dir = self.get_launch_dir()
path = os.path.join(launch_dir, f'{appid_hash}_{version}_{platform}')
if not os.path.isfile(path):
return None
with open(path, 'rb') as f:
raw = f.read()
text = raw.decode('utf-8', errors='replace')
start = text.find('{')
if start < 0:
return None
try:
obj, _ = json.JSONDecoder().raw_decode(text, start)
return obj
except json.JSONDecodeError:
return None
def assert_launch_state(self,
version,
appid_hash,
running,
failures,
platform='windows64'):
"""Assert launch state in .launch/ dir with expected running and failures."""
state = self.read_launch_state(version, appid_hash, platform)
self.assertIsNotNone(state,
f'Launch state not found for {version}/{appid_hash}')
self.assertEqual(running, state.get('running'),
f'Expected running={running}, got {state}')
self.assertEqual(failures, state.get('consecutive_failures'),
f'Expected failures={failures}, got {state}')
last_update = state.get('last_update')
self.assertIsInstance(last_update, str, f'Missing last_update: {state}')
self.assertTrue(last_update.isdecimal(), f'Invalid last_update: {state}')
self.assertGreater(int(last_update), 0, f'Zero last_update: {state}')
def assert_no_launch_state(self, version, appid_hash, platform='windows64'):
"""Assert launch state file does not exist in .launch/ dir."""
launch_dir = self.get_launch_dir()
path = os.path.join(launch_dir, f'{appid_hash}_{version}_{platform}')
self.assertFalse(os.path.exists(path),
f'Launch state file still exists: {path}')
def read_liveness(self, appid_hash, platform='windows64'):
"""Read a health-off liveness-only record from .launch/."""
path = os.path.join(self.get_launch_dir(), f'{appid_hash}_{platform}')
if not os.path.isfile(path):
return None
with open(path, 'rb') as f:
text = f.read().decode('utf-8', errors='replace')
start = text.find('{')
if start < 0:
return None
try:
obj, _ = json.JSONDecoder().raw_decode(text, start)
return obj
except json.JSONDecodeError:
return None
def list_launch_files(self):
"""List all files in the .launch/ directory."""
launch_dir = self.get_launch_dir()
if not os.path.isdir(launch_dir):
return []
return os.listdir(launch_dir)
def read_database(self):
"""Read and parse installer.json from the install directory.
The database file has a CRC32 integrity footer after the JSON.
We use raw_decode to parse only the JSON prefix.
"""
db_path = os.path.join(self.install_dir, 'installer.json')
with open(db_path, 'rb') as f:
raw = f.read()
text = raw.decode('utf-8', errors='replace')
start = text.find('{')
if start < 0:
return {}
obj, _ = json.JSONDecoder().raw_decode(text, start)
return obj
@@ -0,0 +1,249 @@
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Reference parser and correlator for uninstall lifecycle protocol v1."""
import json
import math
import re
import time
MAX_PAYLOAD = 32 * 1024
MAX_ERROR_MESSAGE = 4096
MAX_WARNING = 1024
MAX_WARNINGS = 32
_OPERATION_ID = re.compile(r'^[0-9a-f]{32}$')
_TERMINAL_ERROR_NAMES = {
100: 'CONFIG_ERROR',
101: 'NETWORK_ERROR',
102: 'SIGNATURE_ERROR',
103: 'NO_MATCHING_VERSION',
104: 'EXTRACTION_ERROR',
105: 'INSTALL_ERROR',
106: 'DATABASE_ERROR',
107: 'LOCK_TIMEOUT',
108: 'CANCELLED',
110: 'NO_SENTINEL',
111: 'SENTINEL_READ_ERROR',
112: 'SENTINEL_OWNER_MISMATCH',
113: 'POLICY_DENIED',
114: 'INDEX_ERROR',
115: 'RECOVERY_ERROR',
116: 'REPAIR_ERROR',
117: 'QUARANTINE_ERROR',
118: 'RETENTION_SNAPSHOT_CHANGED',
119: 'POLICY_ERROR',
199: 'UNKNOWN_ERROR',
}
DEFAULT_CORRELATION_CAPACITY = 256
DEFAULT_CORRELATION_EXPIRATION_SECONDS = 30 * 60
def _integer(value, minimum=None, maximum=None):
if isinstance(value, bool) or not isinstance(value, int):
return False
return ((minimum is None or value >= minimum) and
(maximum is None or value <= maximum))
def _uint32_number(value):
if isinstance(value, bool) or not isinstance(value, (int, float)):
return False
if isinstance(value, float):
return (math.isfinite(value) and value.is_integer() and
1 <= value <= 0xffffffff)
return 1 <= value <= 0xffffffff
def _bounded_utf8_string(value, maximum):
if not isinstance(value, str):
return False
try:
return len(value.encode('utf-8')) <= maximum
except UnicodeEncodeError:
return False
def parse_payload(payload):
"""Return (status, event), where status is valid, ignored, or malformed."""
if not isinstance(payload, str) or '\0' in payload:
return 'malformed', None
try:
encoded = payload.encode('utf-8')
except UnicodeEncodeError:
return 'malformed', None
if not encoded or len(encoded) + 1 > MAX_PAYLOAD:
return 'malformed', None
try:
event = json.loads(payload)
except (TypeError, ValueError):
return 'malformed', None
if not isinstance(event, dict):
return 'malformed', None
version = event.get('protocol_version')
name = event.get('event')
if not _integer(version) or not isinstance(name, str):
return 'malformed', None
if version != 1 or name not in ('relaunch_started', 'operation_result'):
return 'ignored', None
operation_id = event.get('operation_id')
if not isinstance(operation_id,
str) or not _OPERATION_ID.fullmatch(operation_id):
return 'malformed', None
if name == 'relaunch_started':
child_pid = event.get('child_pid')
if (not _uint32_number(child_pid) or child_pid < 1 or
child_pid > 0xffffffff):
return 'malformed', None
return 'valid', event
success = event.get('success')
outcome = event.get('outcome')
exit_code = event.get('exit_code')
if (event.get('command') != 'uninstall' or not isinstance(success, bool) or
outcome not in ('committed', 'cleanup_deferred', 'failed') or
not _integer(exit_code)):
return 'malformed', None
error_fields = ('error_code', 'error_name', 'error_message')
if success:
if outcome == 'failed' or exit_code != 0 or any(
k in event for k in error_fields):
return 'malformed', None
else:
error_code = event.get('error_code')
error_name = event.get('error_name')
error_message = event.get('error_message')
if (outcome != 'failed' or not _integer(error_code) or error_code == 0 or
error_code != exit_code or error_code not in _TERMINAL_ERROR_NAMES or
_TERMINAL_ERROR_NAMES[error_code] != error_name or
not _bounded_utf8_string(error_message, MAX_ERROR_MESSAGE)):
return 'malformed', None
warnings = event.get('warnings', [])
if (not isinstance(warnings, list) or len(warnings) > MAX_WARNINGS or
any(not _bounded_utf8_string(item, MAX_WARNING) for item in warnings)):
return 'malformed', None
if ('diagnostics_truncated' in event and
not isinstance(event['diagnostics_truncated'], bool)):
return 'malformed', None
return 'valid', event
class Correlator:
"""Bounded, expiring attribution of terminals to matching handoffs."""
def __init__(self,
capacity=DEFAULT_CORRELATION_CAPACITY,
expiration_seconds=DEFAULT_CORRELATION_EXPIRATION_SECONDS,
clock=time.monotonic):
if capacity < 1 or expiration_seconds <= 0:
raise ValueError('correlation bounds must be positive')
self._capacity = capacity
self._expiration_seconds = expiration_seconds
self._clock = clock
self._last_activity = {}
self.handoffs = {}
self.pending = {}
self.results = {}
def _discard(self, operation_id):
existed = operation_id in self._last_activity
self._last_activity.pop(operation_id, None)
self.handoffs.pop(operation_id, None)
self.pending.pop(operation_id, None)
self.results.pop(operation_id, None)
return existed
def purge_expired(self):
now = self._clock()
expired = [
operation_id
for operation_id, last_activity in self._last_activity.items()
if now - last_activity >= self._expiration_seconds
]
for operation_id in expired:
self._discard(operation_id)
return len(expired)
def _make_room(self, terminal_first):
if len(self._last_activity) < self._capacity:
return True
candidates = [
operation_id for operation_id in self.pending
if operation_id not in self.handoffs
]
if not candidates:
if terminal_first:
return False
candidates = list(self._last_activity)
oldest = min(candidates, key=self._last_activity.__getitem__)
self._discard(oldest)
return True
def accept_payload(self, payload):
status, event = parse_payload(payload)
if status != 'valid':
return status
self.purge_expired()
now = self._clock()
operation_id = event['operation_id']
if event['event'] == 'relaunch_started':
if operation_id in self.handoffs:
return 'duplicate'
if (operation_id not in self._last_activity and
not self._make_room(terminal_first=False)):
return 'ignored'
self.handoffs[operation_id] = event
self._last_activity[operation_id] = now
if operation_id in self.pending:
self.results[operation_id] = self.pending.pop(operation_id)
return 'correlated'
return 'handoff'
if operation_id in self.pending or operation_id in self.results:
return 'duplicate'
if operation_id not in self.handoffs:
if (operation_id not in self._last_activity and
not self._make_room(terminal_first=True)):
return 'ignored'
self.pending[operation_id] = event
self._last_activity[operation_id] = now
return 'buffered'
self.results[operation_id] = event
self._last_activity[operation_id] = now
return 'correlated'
def consume_result(self, operation_id):
self.purge_expired()
result = self.results.get(operation_id)
if result is not None:
self._discard(operation_id)
return result
def remove(self, operation_id):
self.purge_expired()
return self._discard(operation_id)
def tracked_operation_count(self):
self.purge_expired()
return len(self._last_activity)
def snapshot(self):
self.purge_expired()
return {
'handoffs': dict(self.handoffs),
'pending': dict(self.pending),
'results': dict(self.results),
}
def correlate_helper_output(output):
"""Parse the structured helper output and return receiver public state."""
correlator = Correlator()
statuses = []
for payload in output.get('lifecycle', []):
statuses.append(correlator.accept_payload(payload))
state = correlator.snapshot()
return {
'statuses': statuses,
**state,
}
@@ -0,0 +1,314 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Mock client DLL for E2E tests. Loaded by bootstrap.exe in launcher mode.
// Calls RunInstaller (exported by bootstrap.exe) and writes a marker file
// with the result.
//
// Environment variables used:
// CEF_E2E_MARKER_PATH - Path to write the result marker JSON file
// CEF_E2E_CONFIG_JSON - Config JSON to pass to RunInstaller
// CEF_E2E_COMMAND - Command string (default: "install")
// CEF_E2E_THREADING_MODE - "multi" to spawn two threads calling
// RunInstaller sequentially on separate threads
// - "parallel" to spawn two threads calling
// RunInstaller concurrently
// CEF_E2E_CONFIG_JSON_A - Config JSON for thread A (multi/parallel mode)
// CEF_E2E_CONFIG_JSON_B - Config JSON for thread B (multi/parallel mode)
// CEF_E2E_LAUNCH_SUCCESS - When set (with CEF_E2E_EXIT_CODE), call
// RunInstaller("launch_success") to confirm
// launch health before returning the exit code
#include <windows.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include "include/cef_version_info.h"
typedef const char* (*RunInstallerFn)(const char* command,
const char* config_json);
static RunInstallerFn GetRunInstaller() {
HMODULE exe = ::GetModuleHandleW(nullptr);
return reinterpret_cast<RunInstallerFn>(
::GetProcAddress(exe, "RunInstaller"));
}
static std::string GetEnvVar(const char* name) {
char buf[8192] = {};
DWORD len = ::GetEnvironmentVariableA(name, buf, sizeof(buf));
return len > 0 ? std::string(buf, len) : std::string();
}
static std::string WideToJsonEscaped(const wchar_t* wide) {
if (!wide) {
return "";
}
int len =
::WideCharToMultiByte(CP_UTF8, 0, wide, -1, nullptr, 0, nullptr, nullptr);
if (len <= 0) {
return "";
}
std::string utf8(len - 1, '\0');
::WideCharToMultiByte(CP_UTF8, 0, wide, -1, &utf8[0], len, nullptr, nullptr);
std::string escaped;
escaped.reserve(utf8.size());
for (char c : utf8) {
if (c == '\\') {
escaped += "\\\\";
} else if (c == '"') {
escaped += "\\\"";
} else {
escaped += c;
}
}
return escaped;
}
static std::string Utf8ToJsonEscaped(const char* value) {
if (!value) {
return "";
}
std::string escaped;
for (const char* current = value; *current; ++current) {
const unsigned char c = static_cast<unsigned char>(*current);
switch (c) {
case '\\':
escaped += "\\\\";
break;
case '"':
escaped += "\\\"";
break;
case '\b':
escaped += "\\b";
break;
case '\f':
escaped += "\\f";
break;
case '\n':
escaped += "\\n";
break;
case '\r':
escaped += "\\r";
break;
case '\t':
escaped += "\\t";
break;
default:
if (c < 0x20) {
constexpr char kHex[] = "0123456789abcdef";
escaped += "\\u00";
escaped += kHex[c >> 4];
escaped += kHex[c & 0x0F];
} else {
escaped += static_cast<char>(c);
}
}
}
return escaped;
}
static void WriteMarkerFile(const std::string& path,
const char* result_json,
const cef_version_info_t* vi) {
FILE* f = fopen(path.c_str(), "w");
if (!f) {
return;
}
fprintf(f, "{\n");
fprintf(f, " \"result\": %s,\n", result_json ? result_json : "null");
if (vi && vi->size >= CEF_VERSION_INFO_SIZE_WITH_INSTALLER_ERROR) {
std::string escaped_path = WideToJsonEscaped(vi->libcef_path);
fprintf(f, " \"libcef_path\": \"%s\",\n", escaped_path.c_str());
fprintf(f, " \"libcef_is_bundled\": %d,\n", vi->libcef_is_bundled);
fprintf(f, " \"libcef_version_full\": \"%s\",\n",
vi->libcef_version_full ? vi->libcef_version_full : "");
fprintf(f, " \"cef_version_major\": %d,\n", vi->cef_version_major);
fprintf(f, " \"installer_error_code\": %d,\n", vi->installer_error_code);
if (vi->installer_error_message) {
const std::string escaped_message =
Utf8ToJsonEscaped(vi->installer_error_message);
fprintf(f, " \"installer_error_message\": \"%s\"\n",
escaped_message.c_str());
} else {
fprintf(f, " \"installer_error_message\": null\n");
}
} else {
fprintf(f, " \"version_info\": null\n");
}
fprintf(f, "}\n");
fclose(f);
}
// Per-thread context for multi-threaded RunInstaller calls.
struct ThreadContext {
RunInstallerFn run_installer;
std::string command;
std::string config_json;
std::string result_json;
bool success;
};
static DWORD WINAPI RunInstallerThread(LPVOID param) {
auto* ctx = static_cast<ThreadContext*>(param);
const char* result =
ctx->run_installer(ctx->command.c_str(), ctx->config_json.c_str());
ctx->result_json = result ? result : "null";
ctx->success = result && strstr(result, "\"success\":true");
return 0;
}
static void WriteMultiMarkerFile(const std::string& path,
const ThreadContext& ctx_a,
const ThreadContext& ctx_b,
const cef_version_info_t* vi) {
FILE* f = fopen(path.c_str(), "w");
if (!f) {
return;
}
fprintf(f, "{\n");
fprintf(f, " \"result_a\": %s,\n", ctx_a.result_json.c_str());
fprintf(f, " \"result_b\": %s,\n", ctx_b.result_json.c_str());
if (vi) {
fprintf(f, " \"cef_version_major\": %d\n", vi->cef_version_major);
} else {
fprintf(f, " \"version_info\": null\n");
}
fprintf(f, "}\n");
fclose(f);
}
static int RunMultiThreaded(const std::string& marker_path,
const std::string& command,
RunInstallerFn run_installer,
const cef_version_info_t* vi,
bool parallel) {
std::string config_a = GetEnvVar("CEF_E2E_CONFIG_JSON_A");
std::string config_b = GetEnvVar("CEF_E2E_CONFIG_JSON_B");
if (config_a.empty() || config_b.empty()) {
WriteMarkerFile(
marker_path,
"{\"success\":false,\"error_message\":\"missing config A or B\"}", vi);
return 1;
}
ThreadContext ctx_a = {run_installer, command, config_a, "null", false};
ThreadContext ctx_b = {run_installer, command, config_b, "null", false};
HANDLE thread_a =
::CreateThread(nullptr, 0, RunInstallerThread, &ctx_a, 0, nullptr);
if (parallel) {
// Run both threads concurrently. Tests that ScopedThreadPool's refcount
// keeps the pool alive while either thread is still using it.
HANDLE thread_b =
::CreateThread(nullptr, 0, RunInstallerThread, &ctx_b, 0, nullptr);
HANDLE handles[] = {thread_a, thread_b};
::WaitForMultipleObjects(2, handles, TRUE, 60000);
::CloseHandle(thread_a);
::CloseHandle(thread_b);
} else {
// Run sequentially on separate threads. The test goal is verifying
// thread-local result strings don't cross-contaminate.
::WaitForSingleObject(thread_a, 30000);
::CloseHandle(thread_a);
HANDLE thread_b =
::CreateThread(nullptr, 0, RunInstallerThread, &ctx_b, 0, nullptr);
::WaitForSingleObject(thread_b, 30000);
::CloseHandle(thread_b);
}
WriteMultiMarkerFile(marker_path, ctx_a, ctx_b, vi);
return (ctx_a.success && ctx_b.success) ? 0 : 3;
}
extern "C" __declspec(dllexport) int RunWinMain(HINSTANCE hInstance,
LPTSTR lpCmdLine,
int nCmdShow,
void* sandbox_info,
cef_version_info_t* vi) {
std::string marker_path = GetEnvVar("CEF_E2E_MARKER_PATH");
std::string exit_code_str = GetEnvVar("CEF_E2E_EXIT_CODE");
// When CEF_E2E_EXIT_CODE is set, skip RunInstaller entirely. Write the
// marker file with version_info from the bootstrap and return the
// specified exit code. This lets E2E tests control the exit code to
// simulate clean exits, crashes, and neutral exits.
if (!exit_code_str.empty()) {
int exit_code = atoi(exit_code_str.c_str());
// When CEF_E2E_LAUNCH_SUCCESS is set, confirm launch health via the
// RunInstaller export before returning the exit code. This composes with
// CEF_E2E_EXIT_CODE: a confirm-then-crash run leaves the sentinel
// confirmed (running=false, consecutive_failures=0) before the crash exit,
// so the crash does not penalize the CEF version. Mirrors cefclient's
// ConfirmLaunchHealth — kLaunchSuccess ignores config (the sentinel path
// is a process-global the bootstrap set before RunWinMain), so pass null.
if (!GetEnvVar("CEF_E2E_LAUNCH_SUCCESS").empty()) {
RunInstallerFn run_installer = GetRunInstaller();
if (run_installer) {
run_installer("launch_success", nullptr);
}
}
if (!marker_path.empty()) {
WriteMarkerFile(marker_path, "{\"success\":true}", vi);
}
return exit_code;
}
std::string config_json = GetEnvVar("CEF_E2E_CONFIG_JSON");
std::string command = GetEnvVar("CEF_E2E_COMMAND");
std::string threading_mode = GetEnvVar("CEF_E2E_THREADING_MODE");
if (command.empty()) {
command = "install";
}
if (marker_path.empty()) {
return 1;
}
RunInstallerFn run_installer = GetRunInstaller();
if (!run_installer) {
WriteMarkerFile(
marker_path,
"{\"success\":false,\"error_message\":\"RunInstaller not found\"}", vi);
return 2;
}
if (threading_mode == "multi" || threading_mode == "parallel") {
return RunMultiThreaded(marker_path, command, run_installer, vi,
threading_mode == "parallel");
}
// Single-threaded mode (default).
if (config_json.empty()) {
WriteMarkerFile(
marker_path,
"{\"success\":false,\"error_message\":\"no config or marker path\"}",
vi);
return 1;
}
const char* result = run_installer(command.c_str(), config_json.c_str());
WriteMarkerFile(marker_path, result, vi);
if (result && strstr(result, "\"success\":true")) {
return 0;
}
return 3;
}
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved) {
return TRUE;
}
@@ -0,0 +1,203 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <windows.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <string_view>
#include <vector>
namespace {
constexpr ULONG_PTR kProgress = 0x43454649;
constexpr ULONG_PTR kLifecycle = 0x43454652;
constexpr size_t kMaxPayload = 32 * 1024;
struct RawMessage {
ULONG_PTR kind;
std::string payload;
};
std::vector<RawMessage>& Messages() {
static auto* messages = new std::vector<RawMessage>();
return *messages;
}
bool g_cancel = false;
bool g_lifecycle_mode = false;
bool g_close_after_handoff = false;
bool g_non_pumping = false;
LRESULT g_lifecycle_result = 1;
size_t g_rejected = 0;
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
if (msg == WM_COPYDATA) {
const auto* data = reinterpret_cast<const COPYDATASTRUCT*>(lparam);
if (!data || !data->lpData || data->cbData < 2 ||
data->cbData > kMaxPayload ||
(data->dwData != kProgress && data->dwData != kLifecycle)) {
++g_rejected;
return 0;
}
const char* bytes = static_cast<const char*>(data->lpData);
if (bytes[data->cbData - 1] != '\0' ||
std::memchr(bytes, '\0', data->cbData - 1)) {
++g_rejected;
return 0;
}
Messages().push_back({data->dwData, std::string(bytes, data->cbData - 1)});
return data->dwData == kProgress ? (g_cancel ? 2 : 1) : g_lifecycle_result;
}
if (msg == WM_CLOSE) {
::PostQuitMessage(0);
return 0;
}
return ::DefWindowProcW(hwnd, msg, wparam, lparam);
}
void WriteEscaped(FILE* file, std::string_view value) {
std::fputc('"', file);
for (unsigned char c : value) {
if (c == '"' || c == '\\') {
std::fputc('\\', file);
std::fputc(c, file);
} else if (c == '\n') {
std::fputs("\\n", file);
} else if (c == '\r') {
std::fputs("\\r", file);
} else if (c == '\t') {
std::fputs("\\t", file);
} else if (c < 0x20) {
std::fprintf(file, "\\u%04x", c);
} else {
std::fputc(c, file);
}
}
std::fputc('"', file);
}
void WriteArray(FILE* file, ULONG_PTR kind, bool raw_json) {
bool first = true;
std::fputc('[', file);
for (const auto& message : Messages()) {
if (message.kind != kind) {
continue;
}
if (!first) {
std::fputc(',', file);
}
if (raw_json) {
std::fputs(message.payload.c_str(), file);
} else {
WriteEscaped(file, message.payload);
}
first = false;
}
std::fputc(']', file);
}
void WriteOutput(const char* path) {
FILE* file = std::fopen(path, "w");
if (!file) {
return;
}
if (!g_lifecycle_mode) {
WriteArray(file, kProgress, true);
} else {
std::fputs("{\"progress\":", file);
WriteArray(file, kProgress, false);
std::fputs(",\"lifecycle\":", file);
WriteArray(file, kLifecycle, false);
std::fprintf(file, ",\"rejected\":%zu}", g_rejected);
}
std::fputc('\n', file);
std::fclose(file);
}
bool HasEvent(std::string_view event) {
const std::string needle = "\"event\":\"" + std::string(event) + "\"";
for (const auto& message : Messages()) {
if (message.kind == kLifecycle &&
message.payload.find(needle) != std::string::npos) {
return true;
}
}
return false;
}
} // namespace
int main(int argc, char* argv[]) {
if (argc < 2) {
std::fprintf(stderr, "Usage: %s <output_path> [options]\n", argv[0]);
return 1;
}
bool allow_copydata = false;
for (int i = 2; i < argc; ++i) {
const std::string_view arg(argv[i]);
if (arg == "--cancel") {
g_cancel = true;
} else if (arg == "--lifecycle") {
g_lifecycle_mode = true;
} else if (arg == "--close-after-handoff") {
g_close_after_handoff = true;
} else if (arg == "--non-pumping") {
g_non_pumping = true;
} else if (arg == "--allow-copydata") {
allow_copydata = true;
} else if (arg.starts_with("--lifecycle-return=")) {
g_lifecycle_result = static_cast<LRESULT>(std::strtoll(
argv[i] + std::strlen("--lifecycle-return="), nullptr, 10));
}
}
WNDCLASSEXW wc = {};
wc.cbSize = sizeof(wc);
wc.lpfnWndProc = WndProc;
wc.hInstance = ::GetModuleHandleW(nullptr);
wc.lpszClassName = L"CefE2EProgressHelper";
::RegisterClassExW(&wc);
HWND hwnd = ::CreateWindowExW(0, wc.lpszClassName, L"", 0, 0, 0, 0, 0,
HWND_MESSAGE, nullptr, wc.hInstance, nullptr);
if (!hwnd) {
return 1;
}
if (allow_copydata) {
CHANGEFILTERSTRUCT filter = {sizeof(filter)};
::ChangeWindowMessageFilterEx(hwnd, WM_COPYDATA, MSGFLT_ALLOW, &filter);
}
std::printf("%llu\n", static_cast<unsigned long long>(
reinterpret_cast<uintptr_t>(hwnd)));
std::fflush(stdout);
const ULONGLONG start = ::GetTickCount64();
MSG msg;
while (::GetTickCount64() - start < 30000) {
if (g_non_pumping) {
::Sleep(100);
continue;
}
if (::PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
break;
}
::TranslateMessage(&msg);
::DispatchMessageW(&msg);
} else {
::MsgWaitForMultipleObjects(0, nullptr, FALSE, 100, QS_ALLINPUT);
}
// Sent messages can be dispatched while PeekMessageW returns FALSE, so
// inspect copied payloads after either branch and outside WndProc.
if ((HasEvent("operation_result") && HasEvent("relaunch_started")) ||
(g_close_after_handoff && HasEvent("relaunch_started"))) {
break;
}
}
::DestroyWindow(hwnd);
WriteOutput(argv[1]);
return 0;
}
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Runner for CEF installer E2E tests.
Usage:
python3 run_e2e_tests.py --build-dir=out/Debug_GN_x64
python3 run_e2e_tests.py --build-dir=out/Debug_GN_x64 -k test_install_flow
python3 run_e2e_tests.py --build-dir=out/Debug_GN_x64 \
-k "test_launch_success and not clean_exit"
python3 run_e2e_tests.py --build-dir=out/Debug_GN_x64 --verbose
"""
import argparse
import os
import sys
import unittest
def main():
parser = argparse.ArgumentParser(description='Run CEF installer E2E tests')
parser.add_argument(
'--build-dir',
required=True,
help='Path to the build directory (e.g., out/Debug_GN_x64)')
parser.add_argument(
'-k',
'--filter',
default=None,
help='Filter tests by a pytest-style keyword expression (and/or/not, '
'parentheses; bare keywords are case-insensitive substrings of the '
'test id).')
parser.add_argument('--verbose',
'-v',
action='store_true',
help='Verbose test output')
args = parser.parse_args()
# Resolve build dir relative to chromium/src.
build_dir = os.path.abspath(args.build_dir)
if not os.path.isdir(build_dir):
print(f'Error: build directory not found: {build_dir}', file=sys.stderr)
return 1
# Set environment variable for the test base to find build artifacts.
os.environ['CEF_E2E_BUILD_DIR'] = build_dir
# Discover tests in the e2e directory.
test_dir = os.path.dirname(os.path.abspath(__file__))
# Add test_dir to sys.path so `from e2e_test_base import ...` works.
if test_dir not in sys.path:
sys.path.insert(0, test_dir)
loader = unittest.TestLoader()
suite = loader.discover(test_dir, pattern='test_*.py')
if args.filter:
# Import here: e2e_filter lives in test_dir, which was just added to
# sys.path above.
from e2e_filter import compile_filter, flatten_suite
match = compile_filter(args.filter)
suite = unittest.TestSuite(t for t in flatten_suite(suite) if match(str(t)))
verbosity = 2 if args.verbose else 1
runner = unittest.TextTestRunner(verbosity=verbosity)
result = runner.run(suite)
return 0 if result.wasSuccessful() else 1
if __name__ == '__main__':
sys.exit(main())
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""E2E tests for the beta channel install path."""
import glob
import os
import shutil
import unittest
from e2e_test_base import (E2ETestBase, EXIT_SUCCESS, FLAG_HEADLESS,
FLAG_UPDATE)
class TestBetaInstall(E2ETestBase):
def _build_beta_cdn(self, version=None, abi_hash='a1b2c3d4e5f6'):
"""Build a local CDN with beta-channel manifest naming.
Calls the standard build_cdn helper, then copies each platform/abi-hash
manifest to the _beta variant (e.g., 150_windows64.json ->
150_windows64_beta.json). The archive files are channel-agnostic.
"""
if version is None:
version = self.test_version
self.build_cdn(version=version, abi_hash=abi_hash)
for manifest in glob.glob(os.path.join(self.cdn_dir, '*.json')):
name = os.path.basename(manifest)
if name == 'revoked.json':
continue
base, ext = os.path.splitext(name)
beta_name = base + '_beta' + ext
shutil.copy2(manifest, os.path.join(self.cdn_dir, beta_name))
return version, abi_hash
def test_beta_install_uses_beta_manifest(self):
"""Install with channel=beta fetches _beta manifest and succeeds."""
version, abi_hash = self._build_beta_cdn()
appid = 'e2e00030-3030-3030-3030-303030303030'
self.embed_test_config(appid=appid,
vmin=version,
abi_hash=abi_hash,
channel='beta')
exit_code, _, stderr = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(
EXIT_SUCCESS, exit_code, f'Beta install failed (exit {exit_code}): '
f'{stderr.decode(errors="replace")}')
self.assert_version_installed(version)
db = self.read_database()
found = any(a.get('uuid') == appid for a in db.get('apps', []))
self.assertTrue(found, f'App not in database after beta install: {db}')
def test_beta_install_without_beta_manifest_fails(self):
"""Install with channel=beta fails when only stable manifests exist."""
version, abi_hash = self.build_cdn()
appid = 'e2e00031-3131-3131-3131-313131313131'
self.embed_test_config(appid=appid,
vmin=version,
abi_hash=abi_hash,
channel='beta')
exit_code, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
# Should fail because there's no _beta.json manifest.
self.assertNotEqual(EXIT_SUCCESS, exit_code)
self.assert_version_not_installed(version)
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""E2E tests for concurrent installer processes."""
import os
import subprocess
import threading
import unittest
from e2e_test_base import (E2ETestBase, EXIT_SUCCESS, FLAG_DOWNLOAD_PATH,
FLAG_HEADLESS, FLAG_INSTALL_PATH, FLAG_UPDATE)
class TestConcurrent(E2ETestBase):
def test_two_processes_install_simultaneously(self):
"""Two processes install simultaneously, both succeed."""
version, abi_hash = self.build_cdn()
appid_a = 'e2e00024-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
appid_b = 'e2e00024-bbbb-bbbb-bbbb-bbbbbbbbbbbb'
# Create a second executable with an independent embedded resource.
exe_b = os.path.join(self.build_dir, 'CefTestAppB.exe')
self._restore_bootstrap_executable(exe_b)
self.addCleanup(lambda: os.remove(exe_b) if os.path.isfile(exe_b) else None)
# Embed independent resources for both apps before either process starts.
self.embed_test_config(appid=appid_a, vmin=version, abi_hash=abi_hash)
self.embed_test_config(appid=appid_b,
vmin=version,
abi_hash=abi_hash,
exe=exe_b)
results = [None, None]
def run_process(exe, idx):
cmd = [
exe, FLAG_UPDATE, FLAG_HEADLESS,
f'{FLAG_DOWNLOAD_PATH}={self.cdn_dir}',
f'{FLAG_INSTALL_PATH}={self.install_dir}'
]
proc = subprocess.run(cmd,
capture_output=True,
timeout=60,
cwd=self.build_dir)
results[idx] = proc.returncode
t_a = threading.Thread(target=run_process, args=(self.bootstrap_exe, 0))
t_b = threading.Thread(target=run_process, args=(exe_b, 1))
t_a.start()
t_b.start()
t_a.join(timeout=60)
t_b.join(timeout=60)
# Both processes should succeed.
self.assertEqual(EXIT_SUCCESS, results[0],
f'Process A failed: exit {results[0]}')
self.assertEqual(EXIT_SUCCESS, results[1],
f'Process B failed: exit {results[1]}')
# One version directory, two apps in database.
self.assert_version_installed(version)
db = self.read_database()
uuids = [a.get('uuid') for a in db.get('apps', [])]
self.assertIn(appid_a, uuids)
self.assertIn(appid_b, uuids)
@@ -0,0 +1,127 @@
#!/usr/bin/env python3
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Unit tests for e2e_filter (no build directory required)."""
import unittest
from e2e_filter import compile_filter, flatten_suite
class CompileFilterTest(unittest.TestCase):
def test_single_keyword_substring(self):
m = compile_filter('launch')
self.assertTrue(m('test_launch_success (mod.C.test_launch_success)'))
self.assertFalse(m('test_install_flow (mod.C.test_install_flow)'))
def test_case_insensitive(self):
self.assertTrue(compile_filter('LAUNCH')('xx_launch_yy'))
self.assertTrue(compile_filter('launch')('XX_LAUNCH_YY'))
def test_or(self):
m = compile_filter('alpha or beta')
self.assertTrue(m('zz_alpha_zz'))
self.assertTrue(m('zz_beta_zz'))
self.assertFalse(m('zz_gamma_zz'))
def test_and(self):
m = compile_filter('alpha and beta')
self.assertTrue(m('alpha_beta'))
self.assertFalse(m('alpha_only'))
def test_not(self):
m = compile_filter('not alpha')
self.assertTrue(m('beta'))
self.assertFalse(m('alpha'))
def test_parentheses_precedence(self):
m = compile_filter('(alpha or beta) and not gamma')
self.assertTrue(m('alpha_x'))
self.assertTrue(m('beta_x'))
self.assertFalse(m('alpha_gamma'))
self.assertFalse(m('delta'))
def test_precedence_without_parens(self):
# `and` binds tighter than `or`: 'a or (b and c)'.
m = compile_filter('alpha or beta and gamma')
self.assertTrue(m('alpha_only')) # alpha satisfies the or
self.assertTrue(m('beta_gamma')) # beta and gamma together
self.assertFalse(m('beta_only')) # beta without gamma -> false
def test_hyphenated_keyword_falls_back_to_substring(self):
# Hyphen parses as subtraction (BinOp), not a keyword -> literal substring.
m = compile_filter('a-b')
self.assertTrue(m('zz_a-b_zz'))
self.assertFalse(m('zz_ab_zz'))
def test_malformed_expression_falls_back_to_substring(self):
# Trailing operator is a genuine SyntaxError -> literal substring match.
m = compile_filter('alpha or')
self.assertTrue(m('zz_alpha or_zz'))
self.assertFalse(m('zz_alpha_zz'))
def test_python_keyword_falls_back_to_substring(self):
# A bare Python keyword (`import`) is a SyntaxError in eval mode -> literal.
m = compile_filter('import')
self.assertTrue(m('test_import_flow'))
self.assertFalse(m('test_export_flow'))
def test_empty_expression_matches_everything(self):
# '' is a SyntaxError -> substring fallback, and '' is in every string.
# The runner guards this with `if args.filter:`, but pin the module behavior.
m = compile_filter('')
self.assertTrue(m('test_anything'))
self.assertTrue(m(''))
def test_no_match_returns_false(self):
self.assertFalse(compile_filter('nonexistent_xyz')('test_anything'))
def test_non_boolean_expression_falls_back_to_substring(self):
# Parseable but not a boolean expr (binary +) -> literal substring match.
m = compile_filter('a + b')
self.assertTrue(m('zz_a + b_zz'))
self.assertFalse(m('zz_ab_zz'))
def test_dotted_keyword_falls_back_to_substring(self):
# Qualified name parses as ast.Attribute -> literal substring match.
m = compile_filter('mod.Class.test_x')
self.assertTrue(m('test_x (mod.Class.test_x)'))
self.assertFalse(m('test_y (mod.Class.test_y)'))
class FlattenSuiteTest(unittest.TestCase):
class _Dummy(unittest.TestCase):
def test_noop(self):
pass
def test_flattens_nested(self):
inner = unittest.TestSuite([self._Dummy('test_noop')])
outer = unittest.TestSuite([unittest.TestSuite([inner])])
flat = list(flatten_suite(outer))
self.assertEqual(1, len(flat))
self.assertIsInstance(flat[0], self._Dummy)
def test_empty_suite_yields_nothing(self):
self.assertEqual([], list(flatten_suite(unittest.TestSuite())))
def test_flat_suite(self):
suite = unittest.TestSuite([self._Dummy('test_noop')])
flat = list(flatten_suite(suite))
self.assertEqual(1, len(flat))
self.assertIsInstance(flat[0], self._Dummy)
def test_mixed_flat_and_nested_preserves_order(self):
a = self._Dummy('test_noop')
b = self._Dummy('test_noop')
c = self._Dummy('test_noop')
# A bare test, then a nested sub-suite, then another bare test.
suite = unittest.TestSuite([a, unittest.TestSuite([b]), c])
self.assertEqual([a, b, c], list(flatten_suite(suite)))
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""End-to-end coverage for the isolated enterprise policy registry seam.
Tests use HKCU by default so normal E2E runs exercise real registry I/O.
Set CEF_INSTALLER_E2E_POLICY_HKLM=1 in an elevated environment to rerun the
same coverage against the production hive.
"""
import os
import uuid
import winreg
from e2e_test_base import (E2ETestBase, EXIT_POLICY_DENIED, EXIT_POLICY_ERROR,
EXIT_SUCCESS, FLAG_UPDATE)
class TestEnterprisePolicy(E2ETestBase):
def setUp(self):
super().setUp()
self.policy_subkey = ('SOFTWARE\\CEF\\InstallerPolicyTests\\' +
str(uuid.uuid4()))
self.policy_hive_name = 'HKCU'
self.policy_hive = winreg.HKEY_CURRENT_USER
if os.environ.get('CEF_INSTALLER_E2E_POLICY_HKLM') == '1':
self.policy_hive_name = 'HKLM'
self.policy_hive = winreg.HKEY_LOCAL_MACHINE
access = winreg.KEY_READ | winreg.KEY_WRITE | winreg.KEY_WOW64_64KEY
try:
self.policy_key = winreg.CreateKeyEx(self.policy_hive, self.policy_subkey,
0, access)
except PermissionError as error:
self.skipTest(f'isolated {self.policy_hive_name} policy fixture '
f'requires permission: {error}')
self.addCleanup(self._cleanup_policy_key)
def _cleanup_policy_key(self):
if getattr(self, 'policy_key', None):
self.policy_key.Close()
self.policy_key = None
try:
winreg.DeleteKeyEx(self.policy_hive, self.policy_subkey,
winreg.KEY_WOW64_64KEY, 0)
except FileNotFoundError:
pass
def _policy_env(self):
return {
'CEF_INSTALLER_POLICY_TEST_KEY': self.policy_subkey,
'CEF_INSTALLER_POLICY_TEST_HIVE': self.policy_hive_name,
}
def _embed_config(self):
self.embed_test_config('e2e00004-0000-0000-0000-000000000001',
self.test_version,
enable_explicit_modes=True)
def test_enterprise_policy_malformed_returns_119(self):
self._embed_config()
# CdnUrls must be REG_MULTI_SZ; a REG_SZ is a loud policy error.
winreg.SetValueEx(self.policy_key, 'CdnUrls', 0, winreg.REG_SZ,
'https://invalid-type.example/')
code, _, _ = self.run_installer(args=[FLAG_UPDATE], env=self._policy_env())
self.assertEqual(EXIT_POLICY_ERROR, code)
self.assertFalse(os.path.exists(self.install_dir))
def test_enterprise_policy_disable_downloads_denies_update(self):
self._embed_config()
winreg.SetValueEx(self.policy_key, 'DisableDownloads', 0, winreg.REG_DWORD,
1)
code, _, _ = self.run_installer(args=[FLAG_UPDATE], env=self._policy_env())
self.assertEqual(EXIT_POLICY_DENIED, code)
self.assertFalse(os.path.exists(self.install_dir))
def test_enterprise_policy_mirror_overrides_operation_source(self):
version, _ = self.build_cdn()
self.embed_test_config('e2e00004-0000-0000-0000-000000000002',
version,
enable_explicit_modes=True)
winreg.SetValueEx(self.policy_key, 'DownloadPath', 0, winreg.REG_SZ,
self.cdn_dir)
conflicting = os.path.join(self.temp_dir, 'conflicting-empty-mirror')
os.makedirs(conflicting)
code, _, stderr = self.run_installer(
args=[FLAG_UPDATE, f'--cef-download-path={conflicting}'],
use_local_cdn=False,
env=self._policy_env(),
timeout=60)
self.assertEqual(EXIT_SUCCESS, code, stderr.decode(errors='replace'))
self.assert_version_installed(version)
if __name__ == '__main__':
import unittest
unittest.main()
@@ -0,0 +1,91 @@
#!/usr/bin/env python3
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""E2E tests verifying installer exit codes.
Parametrized test covering reachable exit codes.
"""
import os
import unittest
from e2e_test_base import (E2ETestBase, EXIT_CONFIG_ERROR, EXIT_NETWORK_ERROR,
EXIT_NO_MATCHING_VERSION, EXIT_RELAUNCHED,
EXIT_SUCCESS, FLAG_BACKGROUND, FLAG_DOWNLOAD_PATH,
FLAG_HEADLESS, FLAG_UNINSTALL, FLAG_UPDATE)
class TestExitCodes(E2ETestBase):
def test_exit_code_success(self):
"""Exit code 0: Successful install."""
version, abi_hash = self.build_cdn()
self.embed_test_config(
appid='e2e00021-0000-0000-0000-000000000000',
vmin=version,
abi_hash=abi_hash,
)
exit_code, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code)
def test_exit_code_config_error(self):
"""Exit code 100: Invalid config."""
self.embed_raw_config_resource(self.bootstrap_exe, 'NOT VALID JSON {{{')
exit_code, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS],
use_local_cdn=False)
self.assertEqual(EXIT_CONFIG_ERROR, exit_code)
def test_exit_code_network_error(self):
"""Exit code >= 100: Empty CDN causes download/resolve failure."""
self.embed_test_config(
appid='e2e00021-0101-0101-0101-010101010101',
vmin=self.test_version,
abi_hash='a1b2c3d4e5f6',
)
empty_cdn = os.path.join(self.temp_dir, 'empty_cdn')
os.makedirs(empty_cdn)
exit_code, _, _ = self.run_installer(
[FLAG_UPDATE, FLAG_HEADLESS, f'{FLAG_DOWNLOAD_PATH}={empty_cdn}'],
use_local_cdn=False)
# Local download mode with missing manifest reports either
# kExitCodeNetworkError (101) or kExitCodeNoMatchingVersion (103)
# depending on whether the manifest is missing vs empty.
self.assertIn(exit_code, (EXIT_NETWORK_ERROR, EXIT_NO_MATCHING_VERSION))
def test_exit_code_no_matching_version(self):
"""Exit code 103: No CDN version satisfies the config range."""
abi_hash = 'a1b2c3d4e5f6'
self.build_cdn(version=self.test_version, abi_hash=abi_hash)
# vmin higher than what the CDN offers.
self.embed_test_config(
appid='e2e00021-0103-0103-0103-010301030103',
vmin=self.test_version_higher,
abi_hash=abi_hash,
)
exit_code, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_NO_MATCHING_VERSION, exit_code)
def test_exit_code_relaunched(self):
"""Exit code 109: explicit background uninstall starts one child."""
version, abi_hash = self.build_cdn()
appid = 'e2e00021-0109-0109-0109-010901090109'
self.embed_test_config(
appid=appid,
vmin=version,
abi_hash=abi_hash,
)
# Install first.
exit_code, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code)
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
env = {'PATH': self.build_dir + os.pathsep + os.environ.get('PATH', '')}
exit_code, _, stderr = self.run_installer(
[FLAG_UNINSTALL, FLAG_BACKGROUND, FLAG_HEADLESS], env=env)
self.assertEqual(EXIT_RELAUNCHED, exit_code,
stderr.decode(errors='replace'))
self.wait_for_relaunched_uninstall(appid,
relaunch_dirs,
os.path.basename(self.bootstrap_exe),
expected_index_versions=())
@@ -0,0 +1,370 @@
#!/usr/bin/env python3
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""E2E tests for the CEF installer install flow."""
import json
import os
import subprocess
import unittest
from e2e_test_base import (E2ETestBase, EXIT_CONFIG_ERROR,
EXIT_NO_MATCHING_VERSION, EXIT_SUCCESS,
FLAG_HEADLESS, FLAG_UNINSTALL, FLAG_UPDATE)
class TestInstallFlow(E2ETestBase):
def _start_https_cdn_failover(self):
empty_primary = os.path.join(self.temp_dir, 'empty-primary')
os.makedirs(empty_primary)
cert_path, key_path = self.get_loopback_tls_certificate()
primary_url, primary_requests = self.start_https_file_server(
empty_primary, cert_path, key_path)
fallback_url, fallback_requests = self.start_https_file_server(
self.cdn_dir, cert_path, key_path)
return (primary_url, primary_requests, fallback_url, fallback_requests)
def _cdn_resource_config(self, appid, version, abi_hash, cdn_urls):
config = {
'appid': appid,
'vmin': version,
'abi_hash': abi_hash,
'cdn_urls': cdn_urls,
'enable_explicit_modes': True,
}
if self.test_thumbprint:
config['certificate_thumbprint'] = self.test_thumbprint
return config
def _assert_https_archive_failover(self, version, primary_requests,
fallback_requests):
self.assertTrue(primary_requests, 'Primary application CDN got no request')
self.assertTrue(fallback_requests,
'Fallback application CDN got no request')
archive_name = f'cef_{version}_windows64.tar.xz'
self.assertTrue(any(
archive_name in request for request in primary_requests))
self.assertTrue(
any(archive_name in request for request in fallback_requests))
def test_fresh_install(self):
"""Fresh install via /cef-update with a bootstrap resource."""
version, abi_hash = self.build_cdn()
self.embed_test_config(
appid='e2e00001-0001-0001-0001-000000000001',
vmin=version,
abi_hash=abi_hash,
)
exit_code, stdout, stderr = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(
EXIT_SUCCESS, exit_code, f'Install failed (exit {exit_code}): '
f'{stderr.decode(errors="replace")}')
self.assert_version_installed(version)
# Verify database was updated.
db = self.read_database()
self.assertIn('apps', db)
found = any(
a.get('uuid') == 'e2e00001-0001-0001-0001-000000000001'
for a in db['apps'])
self.assertTrue(found, f'App not in database: {db}')
def test_bootstrap_resource_cdn_https_failover(self):
"""Bootstrap-resource URLs drive real process HTTPS failover."""
version, abi_hash = self.build_cdn()
(primary_url, primary_requests, fallback_url,
fallback_requests) = self._start_https_cdn_failover()
app_name = 'E2ECdnBootstrapResource'
app_exe = os.path.join(self.build_dir, f'{app_name}.exe')
self._restore_bootstrap_executable(app_exe)
self.addCleanup(lambda: os.remove(app_exe)
if os.path.isfile(app_exe) else None)
self.embed_config_resource(
app_exe,
self._cdn_resource_config('e2e00032-0032-0032-0032-000000000032',
version, abi_hash,
[primary_url, fallback_url]))
exit_code, _, stderr = self.run_installer(
[FLAG_UPDATE, FLAG_HEADLESS],
exe=app_exe,
use_local_cdn=False,
allow_configured_cdn=True,
timeout=60,
env={'CEF_INSTALLER_IGNORE_CERTIFICATE_ERRORS_FOR_TESTING': '1'})
self.assertEqual(
EXIT_SUCCESS, exit_code,
f'HTTPS bootstrap-resource CDN failover failed: '
f'{stderr.decode(errors="replace")}')
self.assert_version_installed(version)
self._assert_https_archive_failover(version, primary_requests,
fallback_requests)
def test_client_resource_cdn_https_failover(self):
"""Client-resource URLs drive real bootstrap-process HTTPS failover."""
version, abi_hash = self.build_cdn()
(primary_url, primary_requests, fallback_url,
fallback_requests) = self._start_https_cdn_failover()
app_exe, app_name = self.setup_launcher('E2ECdnClientResource')
app_dll = os.path.join(self.build_dir, f'{app_name}.dll')
self.embed_config_resource(
app_dll,
self._cdn_resource_config('e2e00033-0033-0033-0033-000000000033',
version, abi_hash,
[primary_url, fallback_url]))
exit_code, _, stderr = self.run_installer(
[FLAG_UPDATE, FLAG_HEADLESS],
exe=app_exe,
use_local_cdn=False,
allow_configured_cdn=True,
timeout=60,
env={
'CEF_E2E_EXIT_CODE': '0',
'CEF_INSTALLER_IGNORE_CERTIFICATE_ERRORS_FOR_TESTING': '1',
})
self.assertEqual(
EXIT_SUCCESS, exit_code, f'HTTPS client-resource CDN failover failed: '
f'{stderr.decode(errors="replace")}')
self.assert_version_installed(version)
self._assert_https_archive_failover(version, primary_requests,
fallback_requests)
def test_install_idempotency(self):
"""Running install twice succeeds without re-download."""
version, abi_hash = self.build_cdn()
self.embed_test_config(
appid='e2e00003-0003-0003-0003-000000000003',
vmin=version,
abi_hash=abi_hash,
)
# First install.
exit_code1, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code1)
self.assert_version_installed(version)
exit_code2, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code2)
self.assert_version_installed(version)
def test_next_best_when_newest_archive_missing(self):
"""Local mirror propagation lag falls back once."""
abi_hash = 'a1b2c3d4e5f6'
older = self.test_version
newer = self.test_version_update
self.build_cdn(version=older, abi_hash=abi_hash)
self.build_cdn(version=newer, abi_hash=abi_hash)
missing_archive = os.path.join(self.cdn_dir,
f'cef_{newer}_windows64.tar.xz')
self.assertTrue(os.path.isfile(missing_archive))
os.remove(missing_archive)
self.embed_test_config(
appid='e2e00030-0030-0030-0030-000000000030',
vmin=older,
abi_hash=abi_hash,
)
exit_code, _, stderr = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(
EXIT_SUCCESS, exit_code,
f'Next-best install failed: {stderr.decode(errors="replace")}')
self.assert_version_installed(older)
self.assert_version_not_installed(newer)
def test_standalone_auto_install_with_embedded_config(self):
"""Standalone auto-install with embedded config resource."""
version, abi_hash = self.build_cdn()
appid = 'e2e00002-0002-0002-0002-000000000002'
# Create a fresh exe copy with a distinct name for this test.
auto_exe_name = 'E2E2AutoApp'
auto_exe = os.path.join(self.build_dir, f'{auto_exe_name}.exe')
self._restore_bootstrap_executable(auto_exe)
self.addCleanup(lambda: os.remove(auto_exe)
if os.path.isfile(auto_exe) else None)
# Embed config resource into the exe.
config_dict = {
'appid': appid,
'vmin': version,
'abi_hash': abi_hash,
'enable_explicit_modes': True,
}
if self.test_thumbprint:
config_dict['certificate_thumbprint'] = self.test_thumbprint
self.embed_config_resource(auto_exe, config_dict)
# Run with /cef-headless only (no /cef-update) — auto-install triggers because
# no client DLL + config loaded + no explicit command.
exit_code, _, stderr = self.run_installer([FLAG_HEADLESS], exe=auto_exe)
self.assertEqual(
EXIT_SUCCESS, exit_code, f'Auto-install failed (exit {exit_code}): '
f'{stderr.decode(errors="replace")}')
self.assert_version_installed(version)
db = self.read_database()
found = any(a.get('uuid') == appid for a in db.get('apps', []))
self.assertTrue(found, f'App not in database: {db}')
class TestErrorHandling(E2ETestBase):
def test_invalid_config(self):
"""Invalid config resource produces config error 100."""
self.embed_raw_config_resource(self.bootstrap_exe, 'NOT VALID JSON {{{')
exit_code, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS],
use_local_cdn=False)
self.assertEqual(EXIT_CONFIG_ERROR, exit_code)
def test_missing_config(self):
"""A freshly restored bootstrap has no config resource."""
exit_code, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS],
use_local_cdn=False)
self.assertEqual(EXIT_CONFIG_ERROR, exit_code)
def test_revoked_version_only(self):
"""Only available version is revoked, install fails."""
version, abi_hash = self.build_cdn()
# Overwrite revoked.json to revoke the only available version.
revoked_path = os.path.join(self.cdn_dir, 'revoked.json')
revoked = {
'revoked_versions': [{
'version': version,
'reason': 'test',
'revoked_at': '2026-01-01T00:00:00Z'
}]
}
with open(revoked_path, 'w') as f:
json.dump(revoked, f)
self.embed_test_config(
appid='e2e00017-1717-1717-1717-171717171717',
vmin=version,
abi_hash=abi_hash,
)
exit_code, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
# kExitCodeNoMatchingVersion (only version is revoked).
self.assertEqual(EXIT_NO_MATCHING_VERSION, exit_code)
class TestMultiApp(E2ETestBase):
def test_sequential_install_two_apps(self):
"""Sequential install of two apps, shared version."""
version, abi_hash = self.build_cdn()
# Install app A.
self.embed_test_config(
appid='e2e00018-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
vmin=version,
abi_hash=abi_hash,
)
exit_a, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_a)
# Install app B (different appid, same version range).
self.embed_test_config(
appid='e2e00018-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
vmin=version,
abi_hash=abi_hash,
)
exit_b, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_b)
# One version directory, two apps in database.
self.assert_version_installed(version)
db = self.read_database()
uuids = [a.get('uuid') for a in db.get('apps', [])]
self.assertIn('e2e00018-aaaa-aaaa-aaaa-aaaaaaaaaaaa', uuids)
self.assertIn('e2e00018-bbbb-bbbb-bbbb-bbbbbbbbbbbb', uuids)
def test_uninstall_one_of_two_apps(self):
"""Uninstall one of two apps, version survives."""
version, abi_hash = self.build_cdn()
# Install both apps.
for appid in [
'e2e00019-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'e2e00019-bbbb-bbbb-bbbb-bbbbbbbbbbbb'
]:
self.embed_test_config(appid=appid, vmin=version, abi_hash=abi_hash)
exit_code, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code)
# The external executable runs the uninstall synchronously.
self.embed_test_config(
appid='e2e00019-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
vmin=version,
abi_hash=abi_hash,
)
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
exit_u, _, _ = self.run_installer([FLAG_UNINSTALL, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_u)
self.assertEqual(relaunch_dirs, self.snapshot_uninstall_relaunch_dirs())
# Version should still exist (app B needs it).
self.assert_version_installed(version)
# Database should have only app B.
db = self.read_database()
uuids = [a.get('uuid') for a in db.get('apps', [])]
self.assertNotIn('e2e00019-aaaa-aaaa-aaaa-aaaaaaaaaaaa', uuids)
self.assertIn('e2e00019-bbbb-bbbb-bbbb-bbbbbbbbbbbb', uuids)
def test_two_apps_different_milestones(self):
"""Two apps with different milestones, uninstall one."""
# Derive current version dynamically from build_cdn output.
current_ver, hash_a = self.build_cdn(abi_hash='aaa111bbb222')
# Compute next-milestone version (e.g., 150.1 -> 151.0).
parts = current_ver.split('.')
next_milestone = str(int(parts[0]) + 1) + '.0'
hash_b = 'ccc333ddd444'
self.build_cdn(version=next_milestone, abi_hash=hash_b)
appid_a = 'e2e00020-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
appid_b = 'e2e00020-bbbb-bbbb-bbbb-bbbbbbbbbbbb'
# Install App A (current milestone).
self.embed_test_config(appid=appid_a, vmin=current_ver, abi_hash=hash_a)
exit_a, _, stderr = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(
EXIT_SUCCESS, exit_a,
f'App A install failed: {stderr.decode(errors="replace")}')
self.assert_version_installed(current_ver)
# Install App B (next milestone).
self.embed_test_config(appid=appid_b, vmin=next_milestone, abi_hash=hash_b)
exit_b, _, stderr = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(
EXIT_SUCCESS, exit_b,
f'App B install failed: {stderr.decode(errors="replace")}')
# Both version directories should exist.
db = self.read_database()
uuids = [a.get('uuid') for a in db.get('apps', [])]
self.assertIn(appid_a, uuids)
self.assertIn(appid_b, uuids)
# External default uninstall is synchronous.
self.embed_test_config(appid=appid_a, vmin=current_ver, abi_hash=hash_a)
exit_u, _, _ = self.run_installer([FLAG_UNINSTALL, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_u)
# Current milestone pruned, next milestone intact.
self.assert_version_not_installed(current_ver)
db = self.read_database()
uuids = [a.get('uuid') for a in db.get('apps', [])]
self.assertNotIn(appid_a, uuids)
self.assertIn(appid_b, uuids)
@@ -0,0 +1,879 @@
#!/usr/bin/env python3
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""E2E tests for launch health tracking and version rollback.
Uses launcher mode (bootstrap.exe + mock client DLL) with CEF_E2E_EXIT_CODE
to control exit codes and test the sentinel lifecycle.
"""
import json
import os
import shutil
import time
import unittest
from e2e_test_base import (E2ETestBase, EXIT_SUCCESS, FLAG_HEADLESS,
FLAG_UNINSTALL, FLAG_UPDATE, FLAG_FORCECHECK)
# Exit codes the bootstrap treats as success or neutral (see
# IsSuccessOrNeutralExitCode): a launch ending in one of these leaves the
# sentinel running=false. 0 = normal exit; 21 = CEF_RESULT_CODE_PROFILE_IN_USE.
# Any other exit code, without an explicit launch_success confirmation, leaves
# the sentinel running=true.
_SUCCESS_OR_NEUTRAL_EXIT_CODES = frozenset({0, 21})
class TestLaunchHealth(E2ETestBase):
def _embed_launcher_config(self,
app_exe,
appid,
vmin,
abi_hash='a1b2c3d4e5f6',
launch_health='exit_code'):
"""Embed CEF_INSTALLER_CONFIG resource into the launcher exe.
The mock client has no application-config resource, so selection falls
back to this bootstrap resource. This activates the bootstrap's
auto-install path, which is required for launch health sentinel writes.
"""
config = {
'appid': appid,
'vmin': vmin,
'abi_hash': abi_hash,
'launch_health': launch_health,
}
if self.test_thumbprint:
config['certificate_thumbprint'] = self.test_thumbprint
self.embed_config_resource(app_exe, config)
def test_default_off_writes_no_health_sentinel(self):
"""Missing launch_health selects normally without a health sentinel."""
version, abi_hash = self.build_cdn()
appid = 'e2e00a00-0000-0000-0000-000000000000'
self._standalone_install(version, abi_hash, appid)
app_exe, _ = self.setup_launcher('LH00App')
config = {'appid': appid, 'vmin': version, 'abi_hash': abi_hash}
if self.test_thumbprint:
config['certificate_thumbprint'] = self.test_thumbprint
self.embed_config_resource(app_exe, config)
rc, marker = self._run_launcher_with_exit_code(app_exe, 1)
self.assertEqual(1, rc)
self.assertIsNotNone(marker)
self.assert_no_launch_state(version, self.get_appid_hash(appid))
def test_explicit_ordinary_exit_is_neutral(self):
"""Explicit mode requires launch_success even when RunWinMain returns 0."""
version, abi_hash = self.build_cdn()
appid = 'e2e00a00-0000-0000-0000-000000000001'
self._standalone_install(version, abi_hash, appid)
app_exe, _ = self.setup_launcher('LHE0App')
self._embed_launcher_config(app_exe,
appid,
version,
abi_hash,
launch_health='explicit')
rc, _ = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
self.assert_launch_state(version,
self.get_appid_hash(appid),
running=False,
failures=0)
state = self.read_launch_state(version, self.get_appid_hash(appid))
self.assertFalse(state.get('confirmed'))
def _run_launcher_with_exit_code(self,
app_exe,
exit_code,
timeout=30,
launch_success=False):
"""Run launcher with CEF_E2E_EXIT_CODE to simulate a specific exit.
When launch_success=True, also sets CEF_E2E_LAUNCH_SUCCESS so the mock
client calls RunInstaller("launch_success") to confirm launch health
before returning the exit code (simulates a client that confirms CEF
is healthy, possibly before a later crash).
If this launch leaves the sentinel running=true (an unconfirmed failure
exit — not success/neutral, and no launch_success), this method sleeps
briefly before returning. The next launch's scan calls IsProcessAlive()
on this process's PID, and the sleep gives that next process a distinct
creation time so a same-granularity-window PID reuse can't make a dead
crasher look alive. Success/neutral exits and launch_success leave
running=false and are never liveness-checked, so they don't sleep.
"""
marker_path = os.path.join(self.temp_dir, 'e2e_marker.json')
env = {
'CEF_E2E_MARKER_PATH': marker_path,
'CEF_E2E_EXIT_CODE': str(exit_code),
}
if launch_success:
env['CEF_E2E_LAUNCH_SUCCESS'] = '1'
rc, stdout, stderr = self.run_installer(args=[],
exe=app_exe,
use_local_cdn=True,
env=env,
timeout=timeout)
marker_data = None
if os.path.isfile(marker_path):
with open(marker_path) as f:
marker_data = json.load(f)
if exit_code not in _SUCCESS_OR_NEUTRAL_EXIT_CODES and not launch_success:
time.sleep(0.5)
return rc, marker_data
def _standalone_install(self, version, abi_hash, appid, vmin=None, vmax=''):
"""Install a version using standalone /cef-update."""
self.embed_test_config(appid=appid,
vmin=vmin or version,
abi_hash=abi_hash,
vmax=vmax,
launch_health='exit_code',
enable_explicit_modes=True)
exit_code, _, stderr = self.run_installer(
[FLAG_UPDATE, FLAG_HEADLESS, FLAG_FORCECHECK])
self.assertEqual(EXIT_SUCCESS, exit_code,
f'Install failed: {stderr.decode(errors="replace")}')
self.assert_version_installed(version)
def test_fresh_install_clean_exit(self):
"""Install a version, run launcher with exit 0, verify confirmed state."""
version, abi_hash = self.build_cdn()
appid = 'e2e00a01-0001-0001-0001-000000000001'
# Install version via standalone.
self._standalone_install(version, abi_hash, appid)
# Set up launcher.
app_exe, app_name = self.setup_launcher('LH01App')
self._embed_launcher_config(app_exe, appid, version, abi_hash)
# Run launcher with exit code 0.
rc, marker = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
self.assertIsNotNone(marker, 'Marker file not written')
# Verify launch state is confirmed in .launch/ dir.
appid_hash = self.get_appid_hash(appid)
self.assert_launch_state(version, appid_hash, running=False, failures=0)
def test_three_crashes_trigger_rollback(self):
"""Three crashes on newer version trigger rollback to older."""
v1 = self.test_version
v1_1 = self.test_version_update
_, abi_hash = self.build_cdn(version=v1)
self.build_cdn(version=v1_1, abi_hash=abi_hash)
appid = 'e2e00a02-0002-0002-0002-000000000002'
# Install v1 via standalone with vmax pinning.
self._standalone_install(v1, abi_hash, appid, vmin=v1, vmax=v1)
# Set up launcher.
app_exe, app_name = self.setup_launcher('LH02App')
self._embed_launcher_config(app_exe, appid, v1, abi_hash)
# Confirm v1 (exit 0).
rc, _ = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
# Install v1_1 (both versions now available). Use vmin=v1_1 to force
# the installer to install the newer version (v1 already satisfies
# vmin=v1).
self._standalone_install(v1_1, abi_hash, appid, vmin=v1_1)
# Re-embed config with vmin=v1 so both versions are eligible.
self._embed_launcher_config(app_exe, appid, v1, abi_hash)
# Crash v1_1 three times (exit code 1).
for i in range(3):
rc, _ = self._run_launcher_with_exit_code(app_exe, 1)
# Run launcher with exit 0 — should rollback to v1.
rc, marker = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
self.assertIsNotNone(marker)
# Verify v1 is now confirmed in .launch/ dir.
appid_hash = self.get_appid_hash(appid)
self.assert_launch_state(v1, appid_hash, running=False, failures=0)
# v1_1 should still show running=true, failures=2 (sentinel from last crash).
state = self.read_launch_state(v1_1, appid_hash)
self.assertIsNotNone(state)
self.assertTrue(state.get('running'))
self.assertEqual(2, state.get('consecutive_failures'))
def test_neutral_exit_preserves_history(self):
"""Neutral exit preserves failure history without incrementing."""
version, abi_hash = self.build_cdn()
appid = 'e2e00a03-0003-0003-0003-000000000003'
self._standalone_install(version, abi_hash, appid)
app_exe, app_name = self.setup_launcher('LH03App')
self._embed_launcher_config(app_exe, appid, version, abi_hash)
# One crash.
rc, _ = self._run_launcher_with_exit_code(app_exe, 1)
# Neutral exit (code 21 = CEF_RESULT_CODE_PROFILE_IN_USE).
rc, _ = self._run_launcher_with_exit_code(app_exe, 21)
self.assertEqual(21, rc)
# Verify launch state in .launch/ dir: running=false, failures=1 (preserved).
appid_hash = self.get_appid_hash(appid)
self.assert_launch_state(version, appid_hash, running=False, failures=1)
# Run launcher with exit 0 — version still selected (under threshold).
rc, marker = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
def test_clean_exit_deletes_older_launch_files(self):
"""Confirming a newer version deletes older versions' confirmed launch files."""
v1 = self.test_version
v1_1 = self.test_version_update
_, abi_hash = self.build_cdn(version=v1)
self.build_cdn(version=v1_1, abi_hash=abi_hash)
appid = 'e2e00a04-0004-0004-0004-000000000004'
# Install v1 only first, so the launcher picks it (no other choice).
# Use a second app with vmin=v1 to keep global vmin low so GC
# doesn't delete v1's .launch/ file when v1_1 is installed later.
appid2 = 'e2e00a04-0004-0004-0004-000000000002'
self._standalone_install(v1, abi_hash, appid, vmin=v1, vmax=v1)
self.embed_test_config(appid=appid2,
vmin=v1,
vmax=v1,
abi_hash=abi_hash,
enable_explicit_modes=True)
self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
# Confirm v1 via launcher (only version installed).
app_exe, app_name = self.setup_launcher('LH04App')
self._embed_launcher_config(app_exe, appid, v1, abi_hash)
rc, _ = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
appid_hash = self.get_appid_hash(appid)
self.assert_launch_state(v1, appid_hash, running=False, failures=0)
# Now install v1_1. The standalone install's prune won't GC v1's
# file because appid2 keeps global vmin at v1.
self._standalone_install(v1_1, abi_hash, appid, vmin=v1_1)
# Confirm v1_1 — cleanup should delete v1's confirmed file.
self._embed_launcher_config(app_exe, appid, v1, abi_hash)
rc, _ = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
# v1's confirmed launch file should be deleted by cleanup-on-confirm.
self.assert_no_launch_state(v1, appid_hash)
# v1_1's launch file should exist (confirmed).
self.assert_launch_state(v1_1, appid_hash, running=False, failures=0)
def test_previously_confirmed_version_starts_crashing(self):
"""A previously confirmed version can be disqualified if it starts crashing."""
version, abi_hash = self.build_cdn()
appid = 'e2e00a08-0008-0008-0008-000000000008'
self._standalone_install(version, abi_hash, appid)
app_exe, app_name = self.setup_launcher('LH08App')
self._embed_launcher_config(app_exe, appid, version, abi_hash)
# Confirm the version.
rc, _ = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
appid_hash = self.get_appid_hash(appid)
self.assert_launch_state(version, appid_hash, running=False, failures=0)
# Now crash 3 times.
for _ in range(3):
rc, _ = self._run_launcher_with_exit_code(app_exe, 1)
# The version is the only one so it should still be selected (last resort),
# but the launch state should show it was crashing.
state = self.read_launch_state(version, appid_hash)
self.assertIsNotNone(state)
self.assertTrue(state.get('running'))
self.assertEqual(2, state.get('consecutive_failures'))
def test_post_exit_pruning(self):
"""Confirming a newer version allows older to be pruned post-exit."""
v1 = self.test_version
v1_1 = self.test_version_update
_, abi_hash = self.build_cdn(version=v1)
self.build_cdn(version=v1_1, abi_hash=abi_hash)
appid = 'e2e00a05-0005-0005-0005-000000000005'
# Install and confirm v1.
self._standalone_install(v1, abi_hash, appid, vmin=v1, vmax=v1)
app_exe, app_name = self.setup_launcher('LH05App')
self._embed_launcher_config(app_exe, appid, v1, abi_hash)
rc, _ = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
# v1 is confirmed — should be protected from pruning.
self.assert_version_installed(v1)
# Install v1_1 and confirm it.
self._standalone_install(v1_1, abi_hash, appid, vmin=v1_1)
self._embed_launcher_config(app_exe, appid, v1, abi_hash)
rc, _ = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
# v1's launch file was deleted (cleanup on confirm). Post-exit pruning
# should have pruned v1 since it's no longer protected.
self.assert_version_not_installed(v1)
self.assert_version_installed(v1_1)
def test_disqualified_version_stays_disqualified(self):
"""A disqualified version stays disqualified across subsequent launches."""
v1 = self.test_version
v1_1 = self.test_version_update
_, abi_hash = self.build_cdn(version=v1)
self.build_cdn(version=v1_1, abi_hash=abi_hash)
appid = 'e2e00a06-0006-0006-0006-000000000006'
# Install and confirm v1.
self._standalone_install(v1, abi_hash, appid, vmin=v1, vmax=v1)
app_exe, app_name = self.setup_launcher('LH06App')
self._embed_launcher_config(app_exe, appid, v1, abi_hash)
rc, _ = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
# Install v1_1.
self._standalone_install(v1_1, abi_hash, appid, vmin=v1_1)
self._embed_launcher_config(app_exe, appid, v1, abi_hash)
# Crash v1_1 three times.
for _ in range(3):
rc, _ = self._run_launcher_with_exit_code(app_exe, 1)
appid_hash = self.get_appid_hash(appid)
# Record v1_1's launch state after disqualification.
state_after_crashes = self.read_launch_state(v1_1, appid_hash)
self.assertIsNotNone(state_after_crashes)
# Run launcher twice more with exit 0 — v1 should be selected both times.
for _ in range(2):
rc, marker = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
self.assertIsNotNone(marker)
# Marker should show v1 (rollback), not v1_1.
self.assertIn(v1, marker.get('libcef_version_full', ''))
self.assertNotIn(v1_1, marker.get('libcef_version_full', ''))
# v1_1's launch state should be unchanged (still disqualified).
state_now = self.read_launch_state(v1_1, appid_hash)
self.assertEqual(state_after_crashes, state_now)
def test_bundled_preferred_over_disqualified(self):
"""Non-revoked bundled version is selected over a disqualified installed."""
version, abi_hash = self.build_cdn()
appid = 'e2e00a09-0009-0009-0009-000000000009'
# Install and confirm the version.
self._standalone_install(version, abi_hash, appid)
app_exe, app_name = self.setup_launcher('LH09App')
self._embed_launcher_config(app_exe, appid, version, abi_hash)
rc, _ = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
# Crash 3 times to disqualify.
for _ in range(3):
rc, _ = self._run_launcher_with_exit_code(app_exe, 1)
appid_hash = self.get_appid_hash(appid)
state = self.read_launch_state(version, appid_hash)
self.assertIsNotNone(state)
self.assertTrue(state.get('running'))
self.assertEqual(2, state.get('consecutive_failures'))
# Create a bundled CEF directory with the same version.
bundled_dir = os.path.join(self.temp_dir, 'bundled', 'windows64')
release_dir = os.path.join(bundled_dir, 'Release')
os.makedirs(release_dir, exist_ok=True)
with open(os.path.join(bundled_dir, 'cef_version.json'), 'w') as f:
json.dump(
{
'version': version,
'abi_hash': abi_hash,
'platform': 'windows64',
}, f)
with open(os.path.join(release_dir, 'libcef.dll'), 'wb') as f:
f.write(b'fake bundled dll')
with open(os.path.join(bundled_dir, 'catalog.cat'), 'wb') as f:
f.write(b'fake bundled catalog')
# Embed config with bundled_cef_path into the client DLL. The bootstrap
# only parses bundled_cef_path from the client DLL resource (not the exe
# fallback), because allow_bundled_cef_path is only set for that source.
app_dll = os.path.join(self.build_dir, f'{app_name}.dll')
config = {
'appid': appid,
'vmin': version,
'abi_hash': abi_hash,
'bundled_cef_path': bundled_dir,
'launch_health': 'exit_code',
}
if self.test_thumbprint:
config['certificate_thumbprint'] = self.test_thumbprint
self.embed_config_resource(app_dll, config)
# Run launcher — should select bundled over disqualified installed.
rc, marker = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
self.assertIsNotNone(marker, 'Marker file not written')
self.assertEqual(1, marker.get('libcef_is_bundled'),
f'Expected bundled version, got marker: {marker}')
def test_pid_guard_prevents_stale_update(self):
"""PID guard prevents a foreign PID from being treated as a crash."""
version, abi_hash = self.build_cdn()
appid = 'e2e00a07-0007-0007-0007-000000000007'
self._standalone_install(version, abi_hash, appid)
app_exe, app_name = self.setup_launcher('LH07App')
self._embed_launcher_config(app_exe, appid, version, abi_hash)
# Confirm the version.
rc, _ = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
appid_hash = self.get_appid_hash(appid)
self.assert_launch_state(version, appid_hash, running=False, failures=0)
# Overwrite launch state in .launch/ dir with a foreign PID.
launch_dir = self.get_launch_dir()
os.makedirs(launch_dir, exist_ok=True)
ls_path = os.path.join(launch_dir, f'{appid_hash}_{version}_windows64')
foreign_state = {
'appid': appid,
'pid': 99999,
'pid_start_time': '999999999999999',
'consecutive_failures': 2,
'running': True,
'confirmed': False,
'version': version,
'platform': 'windows64',
'last_update': str(self.filetime_now()),
}
self.write_integrity_json(ls_path, foreign_state)
# Run launcher with exit 0. The controller should see the foreign PID
# as a dead process (PID 99999 doesn't exist), compute projected=3,
# and disqualify the version. But since it's the only version, it falls
# back to it. The bootstrap writes a new sentinel with its own PID,
# then confirms on exit 0.
rc, _ = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
# After clean exit, the state should be confirmed in .launch/ dir.
state = self.read_launch_state(version, appid_hash)
self.assertIsNotNone(state)
self.assertFalse(state.get('running'))
self.assertEqual(0, state.get('consecutive_failures'))
# PID should be different from the foreign one.
self.assertNotEqual(99999, state.get('pid'))
# ===========================================================================
# Doom loop prevention
# ===========================================================================
def test_crash_history_survives_pruning(self):
"""Crash history in .launch/ survives version directory pruning."""
v1 = self.test_version
v1_1 = self.test_version_update
v1_2 = self.test_version_higher
_, abi_hash = self.build_cdn(version=v1)
self.build_cdn(version=v1_1, abi_hash=abi_hash)
self.build_cdn(version=v1_2, abi_hash=abi_hash)
appid = 'e2e00a10-0010-0010-0010-000000000010'
# Install v1 and v1_1.
self._standalone_install(v1, abi_hash, appid, vmin=v1, vmax=v1)
self._standalone_install(v1_1, abi_hash, appid, vmin=v1_1, vmax=v1_1)
# Crash v1_1 (one crash, then neutral exit to get running=false).
app_exe, app_name = self.setup_launcher('LH10App')
self._embed_launcher_config(app_exe, appid, v1, abi_hash)
rc, _ = self._run_launcher_with_exit_code(app_exe, 1)
rc, _ = self._run_launcher_with_exit_code(app_exe, 21)
self.assertEqual(21, rc)
appid_hash = self.get_appid_hash(appid)
state_v1_1 = self.read_launch_state(v1_1, appid_hash)
self.assertIsNotNone(state_v1_1)
self.assertEqual(1, state_v1_1.get('consecutive_failures'))
# Register a second app with vmin=v1 to keep the global vmin low
# (GC only deletes below the minimum vmin across ALL apps). Then
# install v1_2 via the first app with vmin=v1_2.
appid2 = 'e2e00a10-0010-0010-0010-000000000002'
self.embed_test_config(appid=appid2,
vmin=v1,
vmax=v1,
abi_hash=abi_hash,
enable_explicit_modes=True)
self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self._standalone_install(v1_2, abi_hash, appid, vmin=v1_2)
self._embed_launcher_config(app_exe, appid, v1, abi_hash)
rc, _ = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
# v1_1's directory should be pruned (not required, not confirmed).
self.assert_version_not_installed(v1_1)
# But v1_1's .launch/ crash history should still exist.
state_after = self.read_launch_state(v1_1, appid_hash)
self.assertIsNotNone(state_after,
'.launch/ crash history should survive pruning')
def test_cleanup_preserves_crash_history(self):
"""Confirming a newer version preserves an older version's crash-history file."""
v1 = self.test_version
v1_1 = self.test_version_update
_, abi_hash = self.build_cdn(version=v1)
self.build_cdn(version=v1_1, abi_hash=abi_hash)
appid = 'e2e00a11-0011-0011-0011-000000000011'
# Install v1 only. Use a second app with vmin=v1 to keep global
# vmin low when v1_1 is installed later.
appid2 = 'e2e00a11-0011-0011-0011-000000000002'
self._standalone_install(v1, abi_hash, appid, vmin=v1, vmax=v1)
self.embed_test_config(appid=appid2,
vmin=v1,
vmax=v1,
abi_hash=abi_hash,
enable_explicit_modes=True)
self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
# Crash v1 once (it's the only version), then neutral exit.
app_exe, app_name = self.setup_launcher('LH11App')
self._embed_launcher_config(app_exe, appid, v1, abi_hash)
rc, _ = self._run_launcher_with_exit_code(app_exe, 1)
rc, _ = self._run_launcher_with_exit_code(app_exe, 21)
self.assertEqual(21, rc)
appid_hash = self.get_appid_hash(appid)
state_v1 = self.read_launch_state(v1, appid_hash)
self.assertIsNotNone(state_v1)
self.assertEqual(1, state_v1.get('consecutive_failures'))
# Install v1_1.
self._standalone_install(v1_1, abi_hash, appid, vmin=v1_1)
# Confirm v1_1 (launcher selects it as newest).
self._embed_launcher_config(app_exe, appid, v1, abi_hash)
rc, _ = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
# v1's crash-history file should be preserved (not deleted by cleanup).
state_v1_after = self.read_launch_state(v1, appid_hash)
self.assertIsNotNone(state_v1_after,
'v1 crash history should survive cleanup')
self.assertEqual(1, state_v1_after.get('consecutive_failures'))
# v1_1 should be confirmed.
self.assert_launch_state(v1_1, appid_hash, running=False, failures=0)
def test_stale_launch_files_gc(self):
"""Recent below-vmin history survives and explicitly old history expires."""
v1 = self.test_version
v1_1 = self.test_version_update
v1_2 = self.test_version_higher
_, abi_hash = self.build_cdn(version=v1)
self.build_cdn(version=v1_1, abi_hash=abi_hash)
appid = 'e2e00a12-0012-0012-0012-000000000012'
# Install v1 and publish recent unconfirmed crash history.
self._standalone_install(v1, abi_hash, appid, vmin=v1, vmax=v1)
appid_hash = self.get_appid_hash(appid)
self.write_health(appid,
v1,
self.filetime_now(),
confirmed=False,
consecutive_failures=1)
# Install v1_1 with vmin=v1_1 (moves the minimum forward).
self._standalone_install(v1_1, abi_hash, appid, vmin=v1_1)
self.assert_version_not_installed(v1)
self.assert_launch_state(v1, appid_hash, running=False, failures=1)
# An already-old record is removed on the first eligible prune.
old_time = self.filetime_now() - 91 * 24 * 60 * 60 * 10000000
self.write_health(appid,
v1,
self.filetime_now(),
last_update=old_time,
confirmed=False,
consecutive_failures=1)
self.build_cdn(version=v1_2, abi_hash=abi_hash)
self._standalone_install(v1_2, abi_hash, appid, vmin=v1_2)
self.assert_no_launch_state(v1, appid_hash)
def test_uninstall_reinstall_retains_disqualification(self):
"""Recent failure history prevents redownloading the same bad version."""
good = self.test_version_update
bad = self.test_version_higher
_, abi_hash = self.build_cdn(version=bad)
appid = 'e2e00a13-0013-0013-0013-000000000013'
self._standalone_install(bad, abi_hash, appid, vmin=bad, vmax=bad)
appid_hash = self.get_appid_hash(appid)
self.write_health(appid,
bad,
self.filetime_now(),
running=True,
confirmed=False,
consecutive_failures=2)
exit_code, _, stderr = self.run_installer(
[FLAG_UNINSTALL, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code,
f'Uninstall failed: {stderr.decode(errors="replace")}')
self.assert_version_not_installed(bad)
self.assert_launch_state(bad, appid_hash, running=True, failures=2)
self.build_cdn(version=good, abi_hash=abi_hash)
cert_path, key_path = self.get_loopback_tls_certificate()
cdn_url, requests = self.start_https_file_server(
self.cdn_dir, cert_path, key_path)
self.embed_test_config(appid=appid,
vmin=good,
vmax=bad,
abi_hash=abi_hash,
launch_health='exit_code',
enable_explicit_modes=True,
cdn_urls=[cdn_url])
exit_code, _, stderr = self.run_installer(
[FLAG_UPDATE, FLAG_HEADLESS, FLAG_FORCECHECK],
use_local_cdn=False,
allow_configured_cdn=True,
timeout=60,
env={'CEF_INSTALLER_IGNORE_CERTIFICATE_ERRORS_FOR_TESTING': '1'})
self.assertEqual(EXIT_SUCCESS, exit_code,
f'Reinstall failed: {stderr.decode(errors="replace")}')
self.assert_version_installed(good)
self.assert_version_not_installed(bad)
good_archive = f'cef_{good}_windows64.tar.xz'
bad_archive = f'cef_{bad}_windows64.tar.xz'
self.assertTrue(any(good_archive in request for request in requests),
f'Good archive was not requested: {requests}')
self.assertFalse(any(bad_archive in request for request in requests),
f'Disqualified archive was requested: {requests}')
# ===========================================================================
# Explicit launch success confirmation (kLaunchSuccess)
# ===========================================================================
def test_launch_success_prevents_crash_penalty(self):
"""launch_success before a crash leaves the version confirmed."""
version, abi_hash = self.build_cdn()
appid = 'e2e00b01-0b01-0b01-0b01-00000000b001'
self._standalone_install(version, abi_hash, appid)
app_exe, app_name = self.setup_launcher('LS01App')
self._embed_launcher_config(app_exe, appid, version, abi_hash)
# First leave a pre-launch running publication behind.
rc, _ = self._run_launcher_with_exit_code(app_exe, 1)
self.assertEqual(1, rc)
appid_hash = self.get_appid_hash(appid)
before = self.read_launch_state(version, appid_hash)
self.assertIsNotNone(before)
before_update = int(before.get('last_update', '0'))
# Client confirms launch health, then the app crashes (exit 1). Because
# the sentinel was confirmed during RunWinMain, the crash exit does not
# revert it: the post-exit handler doesn't run for a failure exit, and the
# sentinel is already running=false, failures=0.
rc, _ = self._run_launcher_with_exit_code(app_exe, 1, launch_success=True)
self.assertEqual(1, rc)
self.assert_launch_state(version, appid_hash, running=False, failures=0)
confirmed = self.read_launch_state(version, appid_hash)
self.assertGreater(int(confirmed.get('last_update', '0')), before_update)
# Next launch: version is confirmed (not disqualified) and selected again.
rc, marker = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
self.assertIsNotNone(marker)
self.assertIn(version, marker.get('libcef_version_full', ''))
def test_launch_success_clean_exit(self):
"""launch_success + clean exit confirms and prunes/cleans up older files."""
v1 = self.test_version
v1_1 = self.test_version_update
_, abi_hash = self.build_cdn(version=v1)
self.build_cdn(version=v1_1, abi_hash=abi_hash)
appid = 'e2e00b02-0b02-0b02-0b02-00000000b002'
# Install v1 only first; keep global vmin low with a second app so v1's
# .launch file isn't GC'd when v1_1 is installed later.
appid2 = 'e2e00b02-0b02-0b02-0b02-000000000002'
self._standalone_install(v1, abi_hash, appid, vmin=v1, vmax=v1)
self.embed_test_config(appid=appid2,
vmin=v1,
vmax=v1,
abi_hash=abi_hash,
enable_explicit_modes=True)
self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
app_exe, app_name = self.setup_launcher('LS02App')
self._embed_launcher_config(app_exe, appid, v1, abi_hash)
rc, _ = self._run_launcher_with_exit_code(app_exe, 0, launch_success=True)
self.assertEqual(0, rc)
appid_hash = self.get_appid_hash(appid)
self.assert_launch_state(v1, appid_hash, running=False, failures=0)
# Install v1_1, then confirm it via launch_success on a clean exit. The
# post-exit handler (exit 0) deletes v1's older confirmed .launch file.
self._standalone_install(v1_1, abi_hash, appid, vmin=v1_1)
self._embed_launcher_config(app_exe, appid, v1, abi_hash)
rc, _ = self._run_launcher_with_exit_code(app_exe, 0, launch_success=True)
self.assertEqual(0, rc)
self.assert_launch_state(v1_1, appid_hash, running=False, failures=0)
self.assert_no_launch_state(v1, appid_hash)
def test_without_launch_success_crash_still_counts(self):
"""Without launch_success, an early crash counts against the version."""
version, abi_hash = self.build_cdn()
appid = 'e2e00b03-0b03-0b03-0b03-00000000b003'
self._standalone_install(version, abi_hash, appid)
app_exe, app_name = self.setup_launcher('LS03App')
self._embed_launcher_config(app_exe, appid, version, abi_hash)
# Crash without confirming: sentinel left running=true (detected next time).
rc, _ = self._run_launcher_with_exit_code(app_exe, 1)
self.assertEqual(1, rc)
appid_hash = self.get_appid_hash(appid)
state = self.read_launch_state(version, appid_hash)
self.assertIsNotNone(state)
self.assertTrue(state.get('running'))
# A second crash launch: the prior crash is projected (failures=1) and
# persisted into the new sentinel before RunWinMain.
rc, _ = self._run_launcher_with_exit_code(app_exe, 1)
self.assertEqual(1, rc)
self.assert_launch_state(version, appid_hash, running=True, failures=1)
def test_launch_success_resets_prior_crash_history(self):
"""launch_success clears a projected failure from a prior crash."""
version, abi_hash = self.build_cdn()
appid = 'e2e00b04-0b04-0b04-0b04-00000000b004'
self._standalone_install(version, abi_hash, appid)
app_exe, app_name = self.setup_launcher('LS04App')
self._embed_launcher_config(app_exe, appid, version, abi_hash)
# Crash once without confirming.
rc, _ = self._run_launcher_with_exit_code(app_exe, 1)
self.assertEqual(1, rc)
appid_hash = self.get_appid_hash(appid)
self.assertTrue(self.read_launch_state(version, appid_hash).get('running'))
# Next launch confirms via launch_success then exits cleanly. The
# projected failure from the prior crash is reset to 0.
rc, _ = self._run_launcher_with_exit_code(app_exe, 0, launch_success=True)
self.assertEqual(0, rc)
self.assert_launch_state(version, appid_hash, running=False, failures=0)
def test_launch_success_then_three_early_crashes_disqualify(self):
"""Confirmation is per-launch: later early crashes still disqualify."""
v1 = self.test_version
v1_1 = self.test_version_update
_, abi_hash = self.build_cdn(version=v1)
self.build_cdn(version=v1_1, abi_hash=abi_hash)
appid = 'e2e00b05-0b05-0b05-0b05-00000000b005'
# Install and confirm v1 (the known-good fallback).
self._standalone_install(v1, abi_hash, appid, vmin=v1, vmax=v1)
app_exe, app_name = self.setup_launcher('LS05App')
self._embed_launcher_config(app_exe, appid, v1, abi_hash)
rc, _ = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
# Install v1_1 and confirm it once via launch_success, then crash (exit 1).
# The confirmation is durable (written during RunWinMain), and the failure
# exit skips post-exit cleanup/prune so v1's protected .launch file — and
# thus the v1 fallback — survives for the rollback assertion below.
self._standalone_install(v1_1, abi_hash, appid, vmin=v1_1)
self._embed_launcher_config(app_exe, appid, v1, abi_hash)
rc, _ = self._run_launcher_with_exit_code(app_exe, 1, launch_success=True)
self.assertEqual(1, rc)
appid_hash = self.get_appid_hash(appid)
self.assert_launch_state(v1_1, appid_hash, running=False, failures=0)
# Now crash v1_1 three times WITHOUT confirming (early crashes). The prior
# confirmation does not carry over — each launch starts fresh.
for _ in range(3):
rc, _ = self._run_launcher_with_exit_code(app_exe, 1)
# Next clean launch: v1_1 is disqualified (projected=3) → rollback to v1.
rc, marker = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
self.assertIsNotNone(marker)
self.assertIn(v1, marker.get('libcef_version_full', ''))
self.assertNotIn(v1_1, marker.get('libcef_version_full', ''))
def test_neutral_exit_does_not_prune(self):
"""Neutral exit does not prune older unused versions; a clean exit does."""
v1 = self.test_version
v1_1 = self.test_version_update
_, abi_hash = self.build_cdn(version=v1)
self.build_cdn(version=v1_1, abi_hash=abi_hash)
appid = 'e2e00b06-0b06-0b06-0b06-00000000b006'
# Install and confirm v1.
self._standalone_install(v1, abi_hash, appid, vmin=v1, vmax=v1)
app_exe, app_name = self.setup_launcher('LS06App')
self._embed_launcher_config(app_exe, appid, v1, abi_hash)
rc, _ = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
self.assert_version_installed(v1)
# Keep the platform global vmin at v1 without requiring v1 once v1_1 is
# installed. This isolates confirmed protection from below-vmin GC.
appid2 = 'e2e00b06-0b06-0b06-0b06-000000000002'
self.embed_test_config(appid=appid2,
vmin=v1,
vmax=v1_1,
abi_hash=abi_hash,
enable_explicit_modes=True)
self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
# Install v1_1 (now the best match; v1 becomes prunable).
self._standalone_install(v1_1, abi_hash, appid, vmin=v1_1)
self._embed_launcher_config(app_exe, appid, v1, abi_hash)
# Run v1_1 with a neutral exit (21 = CEF_RESULT_CODE_PROFILE_IN_USE).
# Pruning must NOT run on a neutral exit (a concurrent live instance may
# hold libcef.dll), so v1 survives.
rc, _ = self._run_launcher_with_exit_code(app_exe, 21)
self.assertEqual(21, rc)
self.assert_version_installed(v1)
self.assert_version_installed(v1_1)
# A following clean exit DOES prune the now-unused v1.
self._embed_launcher_config(app_exe, appid, v1, abi_hash)
rc, _ = self._run_launcher_with_exit_code(app_exe, 0)
self.assertEqual(0, rc)
self.assert_version_not_installed(v1)
self.assert_version_installed(v1_1)
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,704 @@
#!/usr/bin/env python3
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""E2E tests for the CEF installer launcher mode.
Launcher mode: bootstrap.exe is renamed, a client DLL is adjacent.
The bootstrap loads the DLL and calls RunWinMain. The mock client DLL
calls RunInstaller (exported by the bootstrap exe) to query or install CEF.
Tests also cover bootstrap-driven CEF resolution: when a client DLL is
present and installer config is embedded in the bootstrap exe, the
bootstrap resolves the CEF path before loading the client DLL and passes
it via version_info->libcef_path.
"""
import ctypes
import hashlib
import json
import os
import shutil
import time
import unittest
from e2e_test_base import (E2ETestBase, EXIT_CONFIG_ERROR, EXIT_SUCCESS,
FLAG_FORCECHECK, FLAG_HEADLESS, FLAG_UNINSTALL,
FLAG_UPDATE)
class TestLauncherMode(E2ETestBase):
def _make_config_json(self,
appid,
vmin,
abi_hash='a1b2c3d4e5f6',
include_cdn=True,
**extra):
"""Build RunInstaller config JSON for the mock client DLL."""
config = {
'appid': appid,
'vmin': vmin,
'abi_hash': abi_hash,
'install_path': self.install_dir,
'show_progress_ui': False,
}
if include_cdn and os.path.isdir(self.cdn_dir):
config['local_download_path'] = self.cdn_dir
if self.test_thumbprint:
config['certificate_thumbprint'] = self.test_thumbprint
config.update(extra)
return json.dumps(config)
def _run_launcher(self, app_exe, config_json, command='install', timeout=30):
"""Run a launcher exe with mock DLL environment variables.
Returns (exit_code, marker_data) where marker_data is parsed JSON
from the marker file, or None if the file was not written.
"""
marker_path = os.path.join(self.temp_dir, 'e2e_marker.json')
env = {
'CEF_E2E_MARKER_PATH': marker_path,
'CEF_E2E_CONFIG_JSON': config_json,
'CEF_E2E_COMMAND': command,
}
exit_code, stdout, stderr = self.run_installer(args=[],
exe=app_exe,
use_local_cdn=False,
env=env,
timeout=timeout)
marker_data = None
if os.path.isfile(marker_path):
with open(marker_path) as f:
marker_data = json.load(f)
return exit_code, marker_data
def test_dll_finds_installed_version(self):
"""Launcher DLL queries a pre-installed CEF version."""
version, abi_hash = self.build_cdn()
appid = 'e2e00011-1111-1111-1111-111111111111'
# Pre-install using standalone mode.
self.embed_test_config(appid=appid, vmin=version, abi_hash=abi_hash)
exit_code, _, stderr = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code,
f'Pre-install failed: {stderr.decode(errors="replace")}')
self.assert_version_installed(version)
# Set up launcher mode.
app_exe, _ = self.setup_launcher('E2E11App')
# Query via mock DLL.
config_json = self._make_config_json(appid=appid,
vmin=version,
abi_hash=abi_hash,
include_cdn=False)
exit_code, marker = self._run_launcher(app_exe,
config_json,
command='query')
self.assertEqual(EXIT_SUCCESS, exit_code,
f'Launcher query failed (exit {exit_code})')
self.assertIsNotNone(marker, 'Marker file not written')
result = marker.get('result', {})
self.assertTrue(result.get('success'), f'Query not successful: {result}')
self.assertTrue(result.get('libcef_path', ''), 'No libcef_path in result')
def test_dll_installs_cef(self):
"""Launcher DLL installs CEF via RunInstaller."""
version, abi_hash = self.build_cdn()
appid = 'e2e00012-1212-1212-1212-121212121212'
# Set up launcher mode (no pre-install).
app_exe, _ = self.setup_launcher('E2E12App')
# Install via mock DLL.
config_json = self._make_config_json(appid=appid,
vmin=version,
abi_hash=abi_hash)
exit_code, marker = self._run_launcher(app_exe,
config_json,
command='install')
self.assertEqual(EXIT_SUCCESS, exit_code,
f'Launcher install failed (exit {exit_code})')
self.assertIsNotNone(marker, 'Marker file not written')
result = marker.get('result', {})
self.assertTrue(result.get('success'), f'Install not successful: {result}')
self.assert_version_installed(version)
def test_dll_fails_to_install(self):
"""Launcher DLL fails to install with no CDN available."""
appid = 'e2e00013-1313-1313-1313-131313131313'
# Set up launcher mode with no pre-installed version and no CDN.
app_exe, _ = self.setup_launcher('E2E13App')
empty_cdn = os.path.join(self.temp_dir, 'empty_cdn')
os.makedirs(empty_cdn)
config_json = self._make_config_json(appid=appid,
vmin=self.test_version,
include_cdn=False,
local_download_path=empty_cdn)
exit_code, marker = self._run_launcher(app_exe,
config_json,
command='install')
# Mock DLL returns 3 when RunInstaller reports failure.
self.assertNotEqual(EXIT_SUCCESS, exit_code)
self.assertIsNotNone(marker, 'Marker file not written')
result = marker.get('result', {})
self.assertFalse(result.get('success', True))
def test_wrong_thumbprint_fails(self):
"""Wrong certificate thumbprint causes signature error."""
version, abi_hash = self.build_cdn()
appid = 'e2e00016-1616-1616-1616-161616161616'
app_exe, _ = self.setup_launcher('E2E16App')
config_json = self._make_config_json(appid=appid,
vmin=version,
abi_hash=abi_hash,
certificate_thumbprint='a' * 40)
exit_code, marker = self._run_launcher(app_exe,
config_json,
command='install')
self.assertNotEqual(EXIT_SUCCESS, exit_code)
self.assertIsNotNone(marker, 'Marker file not written')
result = marker.get('result', {})
self.assertEqual(102, result.get('error_code'))
self.assertEqual('SIGNATURE_ERROR', result.get('error_name'),
f'Expected SIGNATURE_ERROR, got: {result}')
self.assertEqual('failed', result.get('outcome'))
def test_bundled_cef_fallback(self):
"""Bundled CEF fallback when no CDN available."""
version, abi_hash = self.build_cdn()
appid_pre = 'e2e00004-0004-0004-0004-000000000004'
# Step 1: Standalone install to create version dir on disk.
self.embed_test_config(appid=appid_pre, vmin=version, abi_hash=abi_hash)
exit_code, _, stderr = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code,
f'Pre-install failed: {stderr.decode(errors="replace")}')
self.assert_version_installed(version)
# The version dir at install_dir/Versions/<ver>/windows64/ has
# cef_version.json + Release/libcef.dll — CheckBundledCef structure.
bundled_path = os.path.join(self.install_dir, 'Versions', version,
'windows64')
# Step 2: Launcher mode with bundled_cef_path, empty CDN, fresh install.
fresh_install = os.path.join(self.temp_dir, 'fresh_install')
empty_cdn = os.path.join(self.temp_dir, 'empty_cdn')
os.makedirs(empty_cdn)
appid = 'e2e00004-bbbb-bbbb-bbbb-bbbbbbbbbbbb'
app_exe, _ = self.setup_launcher('E2E4App')
config_json = self._make_config_json(appid=appid,
vmin=version,
abi_hash=abi_hash,
include_cdn=False,
local_download_path=empty_cdn,
install_path=fresh_install,
bundled_cef_path=bundled_path)
exit_code, marker = self._run_launcher(app_exe,
config_json,
command='install')
self.assertEqual(EXIT_SUCCESS, exit_code,
f'Bundled fallback failed (exit {exit_code})')
self.assertIsNotNone(marker, 'Marker file not written')
result = marker.get('result', {})
self.assertTrue(result.get('success'), f'Not successful: {result}')
self.assertTrue(result.get('libcef_path', ''), 'No libcef_path')
class TestBootstrapCefResolution(E2ETestBase):
"""Tests for bootstrap-driven CEF resolution with a client DLL present.
When installer config is embedded in the bootstrap exe, the bootstrap
resolves the installed CEF path and passes it to the client DLL via
version_info->libcef_path — before RunWinMain is called.
"""
def _embed_bootstrap_config(self,
app_exe,
appid,
vmin,
abi_hash='',
enable_explicit_modes=False):
"""Embed a CEF_INSTALLER_CONFIG resource in the bootstrap exe."""
config = {
'appid': appid,
'vmin': vmin,
}
if abi_hash:
config['abi_hash'] = abi_hash
if enable_explicit_modes:
config['enable_explicit_modes'] = True
if self.test_thumbprint:
config['certificate_thumbprint'] = self.test_thumbprint
self.embed_config_resource(app_exe, config)
def _read_marker(self, marker_path):
"""Read and parse the marker file written by the mock client DLL."""
self.assertTrue(os.path.isfile(marker_path),
f'Marker file not written: {marker_path}')
with open(marker_path) as f:
return json.load(f)
def _run_launcher_with_marker(self,
app_exe,
args=None,
env=None,
use_local_cdn=False,
install_path=None,
use_install_path_override=True):
"""Run launcher and return (exit_code, marker_data, stderr)."""
marker_path = os.path.join(self.temp_dir, 'e2e_marker.json')
run_env = {
'CEF_E2E_MARKER_PATH': marker_path,
}
if env:
run_env.update(env)
exit_code, _, stderr = self.run_installer(
args=args or [],
exe=app_exe,
use_local_cdn=use_local_cdn,
install_path=install_path,
use_install_path_override=use_install_path_override,
env=run_env)
marker = None
if os.path.isfile(marker_path):
with open(marker_path) as f:
marker = json.load(f)
return exit_code, marker, stderr
def test_bootstrap_resolves_preinstalled_cef(self):
"""Bootstrap resolves a pre-installed CEF version and passes path via
version_info to the client DLL."""
version, abi_hash = self.build_cdn()
appid = 'e2e0b001-b001-b001-b001-b00100000001'
# Pre-install using standalone mode.
self.embed_test_config(appid=appid, vmin=version, abi_hash=abi_hash)
exit_code, _, stderr = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code,
f'Pre-install failed: {stderr.decode(errors="replace")}')
self.assert_version_installed(version)
# Set up launcher mode with embedded config in bootstrap.
app_exe, _ = self.setup_launcher('BootResolve1')
self._embed_bootstrap_config(app_exe, appid, version, abi_hash)
# Run with no explicit command and no RunInstaller config.
# The bootstrap resolves CEF from embedded config and passes
# version_info->libcef_path to the mock client DLL.
exit_code, marker, _ = self._run_launcher_with_marker(app_exe)
self.assertIsNotNone(marker, 'Mock client DLL was not loaded')
libcef_path = marker.get('libcef_path', '')
self.assertTrue(libcef_path,
f'Bootstrap did not resolve libcef_path: {marker}')
self.assertIn('libcef.dll', libcef_path.lower(),
f'Unexpected libcef_path: {libcef_path}')
version_full = marker.get('libcef_version_full', '')
self.assertIn(version, version_full,
f'libcef_version_full does not contain version: {marker}')
self.assertEqual(0, marker.get('installer_error_code'))
self.assertIsNone(marker.get('installer_error_message'))
liveness = self.read_liveness(self.get_appid_hash(appid))
self.assertIsNotNone(liveness)
self.assertEqual(appid, liveness.get('appid'))
self.assertEqual('windows64', liveness.get('platform'))
self.assertTrue(int(liveness.get('last_launch', '0')) > 0)
def test_bootstrap_local_hit_ignores_held_writer_mutex(self):
"""A local automatic startup launches while another writer owns mutex."""
version, abi_hash = self.build_cdn()
appid = 'e2e0b010-b010-b010-b010-b01000000010'
self.embed_test_config(appid=appid, vmin=version, abi_hash=abi_hash)
exit_code, _, stderr = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code,
f'Pre-install failed: {stderr.decode(errors="replace")}')
app_exe, _ = self.setup_launcher('BootLockFree1')
self._embed_bootstrap_config(app_exe, appid, version, abi_hash)
normalized = self.install_dir.lower().replace('\\', '/')
suffix = hashlib.sha1(normalized.encode('utf-8')).hexdigest()[:16].upper()
mutex_name = f'Global\\CEF_Installer_{suffix}'
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
kernel32.CreateMutexW.argtypes = [
ctypes.c_void_p, ctypes.c_bool, ctypes.c_wchar_p
]
kernel32.CreateMutexW.restype = ctypes.c_void_p
kernel32.ReleaseMutex.argtypes = [ctypes.c_void_p]
kernel32.CloseHandle.argtypes = [ctypes.c_void_p]
mutex = kernel32.CreateMutexW(None, True, mutex_name)
self.assertTrue(mutex, f'CreateMutexW failed: {ctypes.get_last_error()}')
try:
start = time.monotonic()
exit_code, marker, _ = self._run_launcher_with_marker(app_exe)
elapsed = time.monotonic() - start
finally:
kernel32.ReleaseMutex(mutex)
kernel32.CloseHandle(mutex)
self.assertIsNotNone(marker)
self.assertTrue(marker.get('libcef_path'),
f'No resolved path (exit={exit_code}, marker={marker})')
self.assertLess(elapsed, 3.0)
def test_bootstrap_recovers_missing_and_corrupt_version_index(self):
"""Automatic startup can recover a valid distribution without an index."""
version, abi_hash = self.build_cdn()
preinstall_appid = 'e2e0b011-b011-b011-b011-b01100000011'
self.embed_test_config(appid=preinstall_appid,
vmin=version,
abi_hash=abi_hash)
exit_code, _, stderr = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code,
f'Pre-install failed: {stderr.decode(errors="replace")}')
index_path = os.path.join(self.install_dir, 'versions.json')
cases = [('missing', None, 'e2e0b012-b012-b012-b012-b01200000012'),
('corrupt', b'not an index',
'e2e0b013-b013-b013-b013-b01300000013')]
for suffix, corrupt_contents, appid in cases:
with self.subTest(index_state=suffix):
if corrupt_contents is None:
if os.path.exists(index_path):
os.remove(index_path)
else:
with open(index_path, 'wb') as index_file:
index_file.write(corrupt_contents)
app_exe, _ = self.setup_launcher(f'BootIndexRecovery{suffix}')
self._embed_bootstrap_config(app_exe, appid, version, abi_hash)
exit_code, marker, stderr = self._run_launcher_with_marker(app_exe)
self.assertIsNotNone(
marker,
f'Client DLL was not loaded (exit={exit_code}, stderr={stderr!r})')
self.assertTrue(marker.get('libcef_path'), marker)
self.assertIn(version, marker.get('libcef_version_full', ''))
if corrupt_contents is None:
self.assertFalse(os.path.exists(index_path))
else:
with open(index_path, 'rb') as index_file:
self.assertEqual(corrupt_contents, index_file.read())
def test_bootstrap_installs_from_cdn_for_client_dll(self):
"""Bootstrap downloads CEF from CDN when no local version exists,
then passes the path to the client DLL via version_info."""
version, abi_hash = self.build_cdn()
appid = 'e2e0b002-b002-b002-b002-b00200000002'
# Set up launcher mode — no pre-install, CDN available.
app_exe, _ = self.setup_launcher('BootResolve2')
self._embed_bootstrap_config(app_exe, appid, version, abi_hash)
# Run with no explicit command. The bootstrap should auto-install
# from CDN and pass the path via version_info.
exit_code, marker, _ = self._run_launcher_with_marker(app_exe,
use_local_cdn=True)
self.assertIsNotNone(marker, 'Mock client DLL was not loaded')
libcef_path = marker.get('libcef_path', '')
self.assertTrue(libcef_path,
f'Bootstrap did not resolve libcef_path: {marker}')
version_full = marker.get('libcef_version_full', '')
self.assertIn(version, version_full,
f'libcef_version_full does not contain version: {marker}')
self.assert_version_installed(version)
def test_update_falls_through_to_client_dll(self):
"""Explicit /cef-update with a client DLL runs the update and then
loads the client DLL (instead of exiting)."""
version, abi_hash = self.build_cdn()
appid = 'e2e0b003-b003-b003-b003-b00300000003'
# Set up launcher mode with embedded config and CDN.
app_exe, _ = self.setup_launcher('BootResolve3')
self._embed_bootstrap_config(app_exe,
appid,
version,
abi_hash,
enable_explicit_modes=True)
# Run with /cef-update. Previously this would exit without loading
# the client DLL. Now it should run the update, then load the DLL.
exit_code, marker, _ = self._run_launcher_with_marker(
app_exe, args=[FLAG_UPDATE, FLAG_HEADLESS], use_local_cdn=True)
self.assertIsNotNone(marker, 'Client DLL not loaded after /cef-update')
libcef_path = marker.get('libcef_path', '')
self.assertTrue(libcef_path, f'No libcef_path after /cef-update: {marker}')
self.assert_version_installed(version)
self.assert_no_launch_state(version, self.get_appid_hash(appid))
def test_resolution_failure_falls_through(self):
"""When CEF resolution fails (no CDN, no installed version), the
bootstrap still loads the client DLL with empty libcef_path."""
appid = 'e2e0b004-b004-b004-b004-b00400000004'
# Set up launcher mode with NO CDN and NO pre-installed version.
app_exe, _ = self.setup_launcher('BootResolve4')
self._embed_bootstrap_config(app_exe, appid, self.test_version)
empty_cdn = os.path.join(self.temp_dir, 'empty_cdn')
os.makedirs(empty_cdn)
exit_code, marker, _ = self._run_launcher_with_marker(app_exe)
# Client DLL should still be loaded even though resolution failed.
self.assertIsNotNone(marker,
'Client DLL not loaded after resolution failure')
libcef_path = marker.get('libcef_path', '')
self.assertEqual('', libcef_path,
f'Expected empty libcef_path on failure: {marker}')
version_full = marker.get('libcef_version_full', '')
self.assertEqual(
'', version_full,
f'Expected empty libcef_version_full on failure: {marker}')
self.assertIsInstance(marker.get('installer_error_code'), int)
self.assertNotEqual(0, marker.get('installer_error_code'))
self.assertTrue(marker.get('installer_error_message'))
def test_installer_not_configured_clears_error(self):
app_exe, _ = self.setup_launcher('BootNotConfigured')
exit_code, marker, _ = self._run_launcher_with_marker(
app_exe, env={'CEF_E2E_EXIT_CODE': '0'})
self.assertEqual(EXIT_SUCCESS, exit_code)
self.assertIsNotNone(marker)
self.assertEqual('', marker.get('libcef_path', ''))
self.assertEqual(0, marker.get('installer_error_code'))
self.assertIsNone(marker.get('installer_error_message'))
def test_malformed_client_resource_exits_config_error(self):
app_exe, app_name = self.setup_launcher('BootMalformedClient')
self._embed_bootstrap_config(app_exe,
'e2e0b008-b008-b008-b008-b00800000008',
self.test_version)
app_dll = os.path.join(self.build_dir, f'{app_name}.dll')
self.embed_raw_config_resource(app_dll, '{"appid":')
exit_code, marker, _ = self._run_launcher_with_marker(
app_exe, env={'CEF_E2E_EXIT_CODE': '0'})
self.assertEqual(EXIT_CONFIG_ERROR, exit_code)
self.assertIsNone(marker, 'Malformed trusted config loaded the client DLL')
def test_post_clamp_empty_range_exits_config_error(self):
app_exe, app_name = self.setup_launcher('BootEmptyRange')
app_dll = os.path.join(self.build_dir, f'{app_name}.dll')
self.embed_raw_config_resource(
app_dll, '{"appid":"e2e0b009-b009-b009-b009-b00900000009",'
'"vmin":"1.0","vmax":"2.0"}')
exit_code, marker, stderr = self._run_launcher_with_marker(
app_exe, env={'CEF_E2E_EXIT_CODE': '0'})
self.assertEqual(EXIT_CONFIG_ERROR, exit_code)
self.assertIsNone(marker)
diagnostic = stderr.decode(errors='replace')
self.assertIn('Configured vmin 1.0', diagnostic)
self.assertIn('effective vmin', diagnostic)
self.assertIn('configured vmax 2.0', diagnostic)
def test_unchecked_cef_path(self):
"""Bootstrap uses unchecked_cef_path from client DLL resource to resolve
libcef.dll directly, skipping installer version checks."""
version, abi_hash = self.build_cdn()
appid_pre = 'e2e0b005-b005-b005-b005-b00500000005'
# Step 1: Standalone install to create a Release dir with libcef.dll.
self.embed_test_config(appid=appid_pre, vmin=version, abi_hash=abi_hash)
exit_code, _, stderr = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code,
f'Pre-install failed: {stderr.decode(errors="replace")}')
self.assert_version_installed(version)
# The Release dir has libcef.dll — use it as unchecked_cef_path.
unchecked_dir = os.path.join(self.install_dir, 'Versions', version,
'windows64', 'Release')
self.assertTrue(os.path.isfile(os.path.join(unchecked_dir, 'libcef.dll')))
# Step 2: Set up launcher with unchecked_cef_path embedded in the
# client DLL resource. No RunInstaller config — the bootstrap reads
# the DLL resource, resolves the path, and passes it via version_info.
appid = 'e2e0b005-cccc-cccc-cccc-cccccccccccc'
app_exe, app_name = self.setup_launcher('BootResolve5')
app_dll = os.path.join(self.build_dir, f'{app_name}.dll')
dll_config = {
'appid': appid,
'vmin': version,
'abi_hash': abi_hash,
'unchecked_cef_path': unchecked_dir,
}
self.embed_config_resource(app_dll, dll_config)
exit_code, marker, _ = self._run_launcher_with_marker(app_exe)
self.assertIsNotNone(marker, 'Mock client DLL was not loaded')
libcef_path = marker.get('libcef_path', '')
self.assertTrue(libcef_path,
f'Bootstrap did not resolve libcef_path: {marker}')
self.assertIn('libcef.dll', libcef_path.lower(),
f'Unexpected libcef_path: {libcef_path}')
def test_install_path_relative_in_client_dll_resource(self):
"""A client-resource install_path is DLL-relative and exclusive."""
version, abi_hash = self.build_cdn()
app_exe, app_name = self.setup_launcher('BootInstallPath1')
app_dll = os.path.join(self.build_dir, f'{app_name}.dll')
# Only a default-named bootstrap accepts --module. Use the console variant
# so a regression reports on stderr instead of displaying a message box.
# The E2E base's self.bootstrap_exe is a renamed CefTestApp.exe and
# intentionally ignores that switch.
host_exe = os.path.join(self.build_dir, 'bootstrapc.exe')
custom_store = os.path.join(self.build_dir, f'{app_name}_relative_store')
shutil.rmtree(custom_store, ignore_errors=True)
self.addCleanup(lambda: shutil.rmtree(custom_store, ignore_errors=True))
relative_store = os.path.basename(custom_store)
appid = 'e2e0b008-c008-c008-c008-c00800000008'
config = {
'appid': appid,
'vmin': version,
'abi_hash': abi_hash,
'install_path': relative_store,
}
if self.test_thumbprint:
config['certificate_thumbprint'] = self.test_thumbprint
self.embed_config_resource(app_dll, config)
exit_code, marker, stderr = self._run_launcher_with_marker(
app_exe, use_local_cdn=True, use_install_path_override=False)
self.assertIsNotNone(marker, stderr.decode(errors='replace'))
self.assertEqual(0, marker.get('installer_error_code'),
stderr.decode(errors='replace'))
libcef_path = os.path.normcase(os.path.abspath(marker['libcef_path']))
self.assertTrue(
libcef_path.startswith(os.path.normcase(os.path.abspath(custom_store))))
self.assertTrue(os.path.isfile(os.path.join(custom_store,
'installer.json')))
self.assertTrue(os.path.isfile(os.path.join(custom_store, 'versions.json')))
self.assertFalse(os.path.isdir(os.path.join(self.install_dir, 'Versions')))
# The host executable is outside the DLL-relative custom store, so default
# uninstall is synchronous and keeps the already-resolved absolute target.
env = {'PATH': self.build_dir + os.pathsep + os.environ.get('PATH', '')}
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
exit_code, _, stderr = self.run_installer(
args=[f'--module={app_name}', FLAG_UNINSTALL, FLAG_HEADLESS],
exe=host_exe,
use_local_cdn=False,
use_install_path_override=False,
env=env)
self.assertEqual(EXIT_SUCCESS, exit_code, stderr.decode(errors='replace'))
self.assertEqual(relaunch_dirs, self.snapshot_uninstall_relaunch_dirs())
self.assertFalse(
os.path.isdir(os.path.join(custom_store, 'Versions', version)))
def test_bundled_cef_path_in_client_dll_resource(self):
"""Bootstrap resolves bundled_cef_path from client DLL resource, selects
the bundled version when no installed version exists."""
version, abi_hash = self.build_cdn()
appid_pre = 'e2e0b006-b006-b006-b006-b00600000006'
# Step 1: Standalone install to create a version dir with valid metadata.
self.embed_test_config(appid=appid_pre, vmin=version, abi_hash=abi_hash)
exit_code, _, stderr = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code,
f'Pre-install failed: {stderr.decode(errors="replace")}')
self.assert_version_installed(version)
# The version dir has cef_version.json + Release/libcef.dll —
# valid for CheckBundledCef.
bundled_path = os.path.join(self.install_dir, 'Versions', version,
'windows64')
# Step 2: Set up launcher with bundled_cef_path embedded in the client
# DLL resource (not RunInstaller config). Use a fresh install dir so
# there are no pre-installed versions.
fresh_install = os.path.join(self.temp_dir, 'fresh_install')
os.makedirs(fresh_install)
appid = 'e2e0b006-cccc-cccc-cccc-cccccccccccc'
app_exe, app_name = self.setup_launcher('BootBundled1')
app_dll = os.path.join(self.build_dir, f'{app_name}.dll')
dll_config = {
'appid': appid,
'vmin': version,
'abi_hash': abi_hash,
'bundled_cef_path': bundled_path,
}
if self.test_thumbprint:
dll_config['certificate_thumbprint'] = self.test_thumbprint
self.embed_config_resource(app_dll, dll_config)
exit_code, marker, _ = self._run_launcher_with_marker(
app_exe, install_path=fresh_install)
self.assertIsNotNone(marker, 'Mock client DLL was not loaded')
libcef_path = marker.get('libcef_path', '')
self.assertTrue(libcef_path,
f'Bootstrap did not resolve libcef_path: {marker}')
self.assertIn('libcef.dll', libcef_path.lower(),
f'Unexpected libcef_path: {libcef_path}')
# libcef_path should point into the bundled directory, not fresh_install.
norm = lambda p: p.replace('\\', '/').lower()
self.assertIn(norm(bundled_path), norm(libcef_path),
f'libcef_path not from bundled dir: {libcef_path}')
def test_bundled_cef_path_relative_in_client_dll_resource(self):
"""Bootstrap resolves a relative bundled_cef_path against the client
DLL directory."""
import shutil
version, abi_hash = self.build_cdn()
appid_pre = 'e2e0b007-b007-b007-b007-b00700000007'
# Step 1: Standalone install to create version dir.
self.embed_test_config(appid=appid_pre, vmin=version, abi_hash=abi_hash)
exit_code, _, stderr = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code,
f'Pre-install failed: {stderr.decode(errors="replace")}')
self.assert_version_installed(version)
# Copy the bundled version adjacent to the DLL so a relative path works.
app_exe, app_name = self.setup_launcher('BootBundled2')
app_dll = os.path.join(self.build_dir, f'{app_name}.dll')
bundled_rel_dir = os.path.join(self.build_dir, 'bundled_cef')
src_version_dir = os.path.join(self.install_dir, 'Versions', version,
'windows64')
if os.path.isdir(bundled_rel_dir):
shutil.rmtree(bundled_rel_dir)
shutil.copytree(src_version_dir, bundled_rel_dir)
self.addCleanup(lambda: shutil.rmtree(bundled_rel_dir, ignore_errors=True))
# Step 2: Embed a relative bundled_cef_path in the client DLL resource.
fresh_install = os.path.join(self.temp_dir, 'fresh_install2')
os.makedirs(fresh_install)
appid = 'e2e0b007-cccc-cccc-cccc-cccccccccccc'
dll_config = {
'appid': appid,
'vmin': version,
'abi_hash': abi_hash,
'bundled_cef_path': 'bundled_cef',
}
if self.test_thumbprint:
dll_config['certificate_thumbprint'] = self.test_thumbprint
self.embed_config_resource(app_dll, dll_config)
exit_code, marker, _ = self._run_launcher_with_marker(
app_exe, install_path=fresh_install)
self.assertIsNotNone(marker, 'Mock client DLL was not loaded')
libcef_path = marker.get('libcef_path', '')
self.assertTrue(libcef_path,
f'Bootstrap did not resolve libcef_path: {marker}')
self.assertIn('libcef.dll', libcef_path.lower(),
f'Unexpected libcef_path: {libcef_path}')
@@ -0,0 +1,158 @@
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Unit tests for the lifecycle reference receiver."""
import json
import unittest
from lifecycle_receiver import Correlator, parse_payload
ID_A = '0123456789abcdef0123456789abcdef'
ID_B = 'fedcba9876543210fedcba9876543210'
ID_C = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
def handoff(operation_id=ID_A, pid=123):
return json.dumps(
{
'protocol_version': 1,
'event': 'relaunch_started',
'operation_id': operation_id,
'child_pid': pid,
},
separators=(',', ':'))
def terminal(operation_id=ID_A, success=True):
value = {
'protocol_version': 1,
'event': 'operation_result',
'operation_id': operation_id,
'command': 'uninstall',
'success': success,
'outcome': 'committed' if success else 'failed',
'exit_code': 0 if success else 106,
}
if not success:
value.update({
'error_code': 106,
'error_name': 'DATABASE_ERROR',
'error_message': 'Database save failed',
})
return json.dumps(value, separators=(',', ':'))
class TestLifecycleReceiver(unittest.TestCase):
def test_parse_valid_and_unknown(self):
self.assertEqual('valid', parse_payload(handoff())[0])
self.assertEqual('valid', parse_payload(terminal(success=False))[0])
future = json.loads(handoff())
future['protocol_version'] = 2
self.assertEqual('ignored', parse_payload(json.dumps(future))[0])
future['protocol_version'] = 1
future['event'] = 'future'
self.assertEqual('ignored', parse_payload(json.dumps(future))[0])
def test_parse_rejects_malformed_and_bounds(self):
malformed = [
'', '[]', '{',
handoff(pid=0),
handoff(pid=True),
handoff(operation_id=ID_A.upper()),
terminal().replace('"exit_code":0', '"exit_code":1'),
terminal() + '\0'
]
for payload in malformed:
self.assertEqual('malformed', parse_payload(payload)[0], payload)
value = json.loads(terminal(success=False))
value['error_message'] = 'x' * 4097
self.assertEqual('malformed', parse_payload(json.dumps(value))[0])
value = json.loads(terminal())
value['diagnostics_truncated'] = 1
self.assertEqual('malformed', parse_payload(json.dumps(value))[0])
value = json.loads(terminal(success=False))
value['error_message'] = '\ud800'
self.assertEqual('malformed', parse_payload(json.dumps(value))[0])
value['error_message'] = 'safe'
value['warnings'] = ['\ud800']
self.assertEqual('malformed', parse_payload(json.dumps(value))[0])
def test_parse_rejects_impossible_and_unknown_terminal_codes(self):
for error_code, error_name in ((109, 'RELAUNCHED'), (120, 'UNKNOWN_ERROR'),
(9999, 'UNKNOWN_ERROR')):
value = json.loads(terminal(success=False))
value['exit_code'] = error_code
value['error_code'] = error_code
value['error_name'] = error_name
self.assertEqual('malformed', parse_payload(json.dumps(value))[0], value)
value = json.loads(terminal(success=False))
value['exit_code'] = 199
value['error_code'] = 199
value['error_name'] = 'UNKNOWN_ERROR'
self.assertEqual('valid', parse_payload(json.dumps(value))[0])
def test_terminal_first_duplicate_and_missing_handoff(self):
receiver = Correlator()
self.assertEqual('buffered', receiver.accept_payload(terminal()))
self.assertNotIn(ID_A, receiver.results)
self.assertEqual('duplicate', receiver.accept_payload(terminal()))
self.assertEqual('correlated', receiver.accept_payload(handoff()))
self.assertIn(ID_A, receiver.results)
self.assertEqual('duplicate', receiver.accept_payload(terminal()))
self.assertEqual('duplicate', receiver.accept_payload(handoff()))
self.assertEqual('buffered', receiver.accept_payload(terminal(ID_B)))
self.assertNotIn(ID_B, receiver.results)
def test_concurrent_operation_ids_do_not_cross_correlate(self):
receiver = Correlator()
self.assertEqual('handoff', receiver.accept_payload(handoff(ID_A, 1)))
self.assertEqual('handoff', receiver.accept_payload(handoff(ID_B, 2)))
self.assertEqual('correlated', receiver.accept_payload(terminal(ID_B)))
self.assertNotIn(ID_A, receiver.results)
self.assertEqual('correlated', receiver.accept_payload(terminal(ID_A)))
self.assertEqual(ID_A, receiver.results[ID_A]['operation_id'])
self.assertEqual(ID_B, receiver.results[ID_B]['operation_id'])
def test_correlation_is_bounded_expiring_and_consumable(self):
now = [100.0]
receiver = Correlator(capacity=2,
expiration_seconds=10,
clock=lambda: now[0])
self.assertEqual('buffered', receiver.accept_payload(terminal(ID_A)))
now[0] += 1
self.assertEqual('handoff', receiver.accept_payload(handoff(ID_B, 2)))
now[0] += 1
self.assertEqual('buffered', receiver.accept_payload(terminal(ID_C)))
self.assertEqual(2, receiver.tracked_operation_count())
# Prefer evicting the oldest orphan terminal over an active handoff.
self.assertEqual('handoff', receiver.accept_payload(handoff(ID_A, 1)))
self.assertNotIn(ID_A, receiver.results)
self.assertNotIn(ID_C, receiver.pending)
self.assertIn(ID_B, receiver.handoffs)
self.assertEqual('ignored', receiver.accept_payload(terminal(ID_C)))
self.assertEqual(2, receiver.tracked_operation_count())
self.assertEqual('correlated', receiver.accept_payload(terminal(ID_A)))
self.assertEqual(ID_A, receiver.consume_result(ID_A)['operation_id'])
self.assertNotIn(ID_A, receiver.handoffs)
self.assertNotIn(ID_A, receiver.results)
self.assertTrue(receiver.remove(ID_B))
self.assertFalse(receiver.remove(ID_B))
self.assertEqual('buffered', receiver.accept_payload(terminal(ID_C)))
self.assertTrue(receiver.remove(ID_C))
self.assertEqual(0, receiver.tracked_operation_count())
self.assertEqual('buffered', receiver.accept_payload(terminal(ID_C)))
now[0] += 10
self.assertEqual(1, receiver.purge_expired())
self.assertEqual(0, receiver.tracked_operation_count())
self.assertEqual('handoff', receiver.accept_payload(handoff(ID_C, 3)))
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""E2E tests for WM_COPYDATA progress notification."""
import ctypes
import json
import os
import subprocess
import time
import unittest
from e2e_test_base import (E2ETestBase, EXIT_SUCCESS, FLAG_HEADLESS,
FLAG_PARENT, FLAG_UPDATE)
class TestProgress(E2ETestBase):
def test_parent_receives_progress(self):
"""Parent window receives WM_COPYDATA progress updates."""
helper_exe = os.path.join(self.build_dir, 'cef_e2e_progress_helper.exe')
if not os.path.isfile(helper_exe):
self.skipTest('cef_e2e_progress_helper.exe not found')
version, abi_hash = self.build_cdn()
appid = 'e2e00025-2525-2525-2525-252525252525'
self.embed_test_config(appid=appid, vmin=version, abi_hash=abi_hash)
messages_path = os.path.join(self.temp_dir, 'progress_messages.json')
# Start the progress helper — it prints HWND to stdout then pumps.
helper_proc = subprocess.Popen([helper_exe, messages_path],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=self.build_dir)
try:
# Read HWND from first line of stdout.
hwnd_line = helper_proc.stdout.readline().decode().strip()
self.assertTrue(hwnd_line, 'No HWND from progress helper')
hwnd = int(hwnd_line)
# Run installer with parent window handle.
exit_code, _, stderr = self.run_installer(
[FLAG_UPDATE, FLAG_HEADLESS, f'{FLAG_PARENT}={hwnd}'])
self.assertEqual(EXIT_SUCCESS, exit_code,
f'Install failed: {stderr.decode(errors="replace")}')
finally:
# Send WM_CLOSE for clean shutdown (helper writes output on exit).
WM_CLOSE = 0x0010
ctypes.windll.user32.PostMessageW(hwnd, WM_CLOSE, 0, 0)
helper_proc.wait(timeout=10)
# Read collected messages.
self.assertTrue(os.path.isfile(messages_path),
'Progress messages file not written')
with open(messages_path) as f:
messages = json.load(f)
self.assertIsInstance(messages, list)
self.assertGreater(len(messages), 0, 'No progress messages received')
# Verify message structure.
for msg in messages:
self.assertIn('step_name', msg, f'Missing step_name: {msg}')
self.assertIn('step', msg, f'Missing step: {msg}')
self.assertIn('total_steps', msg, f'Missing total_steps: {msg}')
self.assertIn('bytes_done', msg, f'Missing bytes_done: {msg}')
self.assertIn('bytes_total', msg, f'Missing bytes_total: {msg}')
self.assertIn('overall_percent', msg, f'Missing overall_percent: {msg}')
self.assertGreaterEqual(msg['overall_percent'], 0)
self.assertLessEqual(msg['overall_percent'], 100)
self.assertIn('message', msg, f'Missing message field: {msg}')
@@ -0,0 +1,168 @@
#!/usr/bin/env python3
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""E2E tests for explicit registration-retention maintenance."""
import json
import os
from e2e_test_base import (E2ETestBase, EXIT_CONFIG_ERROR, EXIT_SUCCESS,
FLAG_RETENTION_APPLY, FLAG_RETENTION_DRY_RUN,
FLAG_RETENTION_MAX_AGE_DAYS)
class TestRetention(E2ETestBase):
def _run_export(self, app_exe, config, command, marker_name):
marker_path = os.path.join(self.temp_dir, marker_name)
env = {
'CEF_E2E_MARKER_PATH': marker_path,
'CEF_E2E_CONFIG_JSON': json.dumps(config),
'CEF_E2E_COMMAND': command,
}
self.run_installer(exe=app_exe, use_local_cdn=False, env=env, timeout=60)
self.assertTrue(os.path.isfile(marker_path), 'Marker file not written')
with open(marker_path) as f:
return json.load(f)['result']
def _install(self, app_exe, appid, version, abi_hash, marker):
config = {
'appid': appid,
'vmin': version,
'abi_hash': abi_hash,
'install_path': self.install_dir,
'local_download_path': self.cdn_dir,
'show_progress_ui': False,
}
if self.test_thumbprint:
config['certificate_thumbprint'] = self.test_thumbprint
result = self._run_export(app_exe, config, 'install', marker)
self.assertTrue(result.get('success'), result)
def _retention(self, app_exe, command, marker, days=180):
return self._run_export(app_exe, {
'install_path': self.install_dir,
'max_age_days': days,
}, command, marker)
def _store_snapshot(self):
snapshot = {}
if not os.path.isdir(self.install_dir):
return snapshot
for root, _, files in os.walk(self.install_dir):
for name in files:
if name == 'cef_installer.log':
continue
path = os.path.join(root, name)
with open(path, 'rb') as f:
snapshot[os.path.relpath(path, self.install_dir)] = f.read()
return snapshot
def test_dry_run_and_apply(self):
version, abi_hash = self.build_cdn()
appid = 'a1000000-0000-4000-8000-000000000001'
app_exe, _ = self.setup_launcher('RetentionE2EApply')
self._install(app_exe, appid, version, abi_hash, 'install.json')
evidence_path = self.write_liveness(
appid,
self.filetime_now() - 181 * 24 * 60 * 60 * 10000000)
before = self._store_snapshot()
dry_run = self._retention(app_exe, 'retention_dry_run', 'dry_run.json')
self.assertTrue(dry_run.get('success'), dry_run)
self.assertEqual('reclaim', dry_run['registrations'][0]['decision'])
self.assertTrue(dry_run['versions'][0]['expected_removal'])
self.assertEqual(before, self._store_snapshot())
apply = self._retention(app_exe, 'retention_apply', 'apply.json')
self.assertTrue(apply.get('success'), apply)
self.assertTrue(apply.get('registrations_committed'))
self.assertTrue(apply.get('versions_pruned'))
self.assertEqual([], self.read_database().get('apps'))
self.assertFalse(os.path.exists(evidence_path))
self.assert_version_not_installed(version)
def test_unknown_evidence_preserves_shared_version(self):
version, abi_hash = self.build_cdn()
old_appid = 'a1000000-0000-4000-8000-000000000002'
unknown_appid = 'a1000000-0000-4000-8000-000000000003'
app_exe, _ = self.setup_launcher('RetentionE2EShared')
self._install(app_exe, old_appid, version, abi_hash, 'install.json')
self.write_registration_database([{
'uuid': old_appid,
'platform': 'windows64',
'vmin': version,
'vmax': '',
'abi_hash': abi_hash,
}, {
'uuid': unknown_appid,
'platform': 'windows64',
'vmin': version,
'vmax': '',
'abi_hash': abi_hash,
}])
self.write_liveness(old_appid,
self.filetime_now() - 181 * 24 * 60 * 60 * 10000000)
result = self._retention(app_exe, 'retention_apply', 'apply.json')
self.assertTrue(result.get('success'), result)
apps = self.read_database().get('apps')
self.assertEqual([unknown_appid], [app['uuid'] for app in apps])
self.assert_version_installed(version)
decisions = {
item['appid']: item['decision'] for item in result['registrations']
}
self.assertEqual('reclaim', decisions[old_appid])
self.assertEqual('protected', decisions[unknown_appid])
def test_fresh_evidence_between_dry_run_and_apply_rescues(self):
version, abi_hash = self.build_cdn()
appid = 'a1000000-0000-4000-8000-000000000004'
app_exe, _ = self.setup_launcher('RetentionE2ERescue')
self._install(app_exe, appid, version, abi_hash, 'install.json')
self.write_liveness(appid,
self.filetime_now() - 181 * 24 * 60 * 60 * 10000000)
dry_run = self._retention(app_exe, 'retention_dry_run', 'dry_run.json')
self.assertEqual('reclaim', dry_run['registrations'][0]['decision'])
self.write_liveness(appid, self.filetime_now())
apply = self._retention(app_exe, 'retention_apply', 'apply.json')
self.assertTrue(apply.get('success'), apply)
self.assertEqual([], [
item for item in apply['registrations'] if item['decision'] == 'reclaim'
])
self.assertEqual(1, len(self.read_database().get('apps')))
self.assert_version_installed(version)
def test_cli_custom_store_and_threshold_validation(self):
version, abi_hash = self.build_cdn()
appid = 'a1000000-0000-4000-8000-000000000005'
self.install_dir = os.path.join(self.temp_dir, 'Program Files', 'CEF')
app_exe, _ = self.setup_launcher('RetentionE2ECliInstall')
self._install(app_exe, appid, version, abi_hash, 'install.json')
self.write_liveness(appid,
self.filetime_now() - 91 * 24 * 60 * 60 * 10000000)
self.embed_test_config(appid, version, abi_hash)
code, stdout, _ = self.run_installer(
args=[FLAG_RETENTION_DRY_RUN, f'{FLAG_RETENTION_MAX_AGE_DAYS}=90'])
self.assertEqual(EXIT_SUCCESS, code)
report = json.loads(stdout.decode('utf-8'))
self.assertEqual('reclaim', report['registrations'][0]['decision'])
before = self._store_snapshot()
code, _, _ = self.run_installer(
args=[FLAG_RETENTION_APPLY, f'{FLAG_RETENTION_MAX_AGE_DAYS}=89'])
self.assertEqual(EXIT_CONFIG_ERROR, code)
self.assertEqual(before, self._store_snapshot())
if __name__ == '__main__':
import unittest
unittest.main()
@@ -0,0 +1,375 @@
#!/usr/bin/env python3
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""E2E tests for the RunInstaller DLL export."""
import json
import os
import sys
import unittest
from e2e_test_base import E2ETestBase, EXIT_SUCCESS
class TestRunInstaller(E2ETestBase):
def _run_export(self, app_exe, config, command, marker_name, extra_env=None):
marker_path = os.path.join(self.temp_dir, marker_name)
env = {
'CEF_E2E_MARKER_PATH': marker_path,
'CEF_E2E_CONFIG_JSON': json.dumps(config),
'CEF_E2E_COMMAND': command,
}
if extra_env:
env.update(extra_env)
self.run_installer(exe=app_exe, use_local_cdn=False, env=env, timeout=60)
self.assertTrue(os.path.isfile(marker_path), 'Marker file not written')
with open(marker_path) as f:
return json.load(f)['result']
def test_run_installer_returns_valid_json(self):
"""RunInstaller returns valid JSON with success and libcef_path."""
version, abi_hash = self.build_cdn()
appid = 'e2e00022-2222-2222-2222-222222222222'
app_exe, _ = self.setup_launcher('E2E22App')
config = {
'appid': appid,
'vmin': version,
'abi_hash': abi_hash,
'install_path': self.install_dir,
'local_download_path': self.cdn_dir,
'show_progress_ui': False,
}
if self.test_thumbprint:
config['certificate_thumbprint'] = self.test_thumbprint
config_json = json.dumps(config)
marker_path = os.path.join(self.temp_dir, 'e2e_marker.json')
env = {
'CEF_E2E_MARKER_PATH': marker_path,
'CEF_E2E_CONFIG_JSON': config_json,
'CEF_E2E_COMMAND': 'install',
}
exit_code, _, stderr = self.run_installer(args=[],
exe=app_exe,
use_local_cdn=False,
env=env)
self.assertEqual(
EXIT_SUCCESS, exit_code, f'RunInstaller failed (exit {exit_code}): '
f'{stderr.decode(errors="replace")}')
self.assertTrue(os.path.isfile(marker_path), 'Marker file not written')
with open(marker_path) as f:
marker = json.load(f)
# Result is valid JSON with expected fields.
result = marker.get('result', {})
self.assertIsInstance(result, dict)
self.assertTrue(result.get('success'), f'Not successful: {result}')
self.assertEqual('committed', result.get('outcome'))
self.assertNotIn('restart_recommended', result)
self.assertTrue(result.get('libcef_path'), 'No libcef_path')
self.assertIn('installed_version', result)
self.assertIn(version, result.get('version_full', ''),
f'version_full does not contain version: {result}')
# Version was actually installed on disk.
self.assert_version_installed(version)
def test_expanded_index_failure_is_hard_and_retryable(self):
"""Checked expanded-index failure leaves no fabricated registration."""
version, abi_hash = self.build_cdn()
appid = 'e2e0f001-f001-f001-f001-f001f001f001'
app_exe, _ = self.setup_launcher('E2EFaultIndexApp')
os.makedirs(self.install_dir, exist_ok=True)
with open(os.path.join(self.install_dir, 'versions.json'), 'w') as f:
json.dump({'versions': []}, f)
config = {
'appid': appid,
'vmin': version,
'abi_hash': abi_hash,
'install_path': self.install_dir,
'local_download_path': self.cdn_dir,
'show_progress_ui': False,
}
if self.test_thumbprint:
config['certificate_thumbprint'] = self.test_thumbprint
failed = self._run_export(app_exe, config, 'install',
'fault_index_failed.json',
{'CEF_INSTALLER_TEST_INDEX_FAULT': 'write'})
self.assertFalse(failed['success'])
self.assertEqual('failed', failed['outcome'])
self.assertEqual(114, failed['error_code'])
self.assertEqual('INDEX_ERROR', failed['error_name'])
self.assertFalse(
os.path.exists(os.path.join(self.install_dir, 'installer.json')))
retried = self._run_export(app_exe, config, 'install',
'fault_index_retry.json')
self.assertTrue(retried['success'], retried)
self.assertEqual('committed', retried['outcome'])
self.assert_version_installed(version)
def test_database_failure_leaves_safe_partial_add(self):
"""Directory/index publication precedes the required registration save."""
version, abi_hash = self.build_cdn()
appid = 'e2e0f002-f002-f002-f002-f002f002f002'
app_exe, _ = self.setup_launcher('E2EFaultDatabaseApp')
config = {
'appid': appid,
'vmin': version,
'abi_hash': abi_hash,
'install_path': self.install_dir,
'local_download_path': self.cdn_dir,
'show_progress_ui': False,
}
if self.test_thumbprint:
config['certificate_thumbprint'] = self.test_thumbprint
failed = self._run_export(app_exe, config, 'install',
'fault_database_failed.json',
{'CEF_INSTALLER_TEST_DATABASE_SAVE_FAILURE': '1'})
self.assertFalse(failed['success'])
self.assertEqual('DATABASE_ERROR', failed['error_name'])
self.assert_version_installed(version)
retried = self._run_export(app_exe, config, 'install',
'fault_database_retry.json')
self.assertTrue(retried['success'], retried)
db = self.read_database()
self.assertIn(appid, [entry['uuid'] for entry in db.get('apps', [])])
def test_trash_reclamation_reports_cleanup_deferred(self):
"""Logical uninstall commits before faulted physical trash reclamation."""
version, abi_hash = self.build_cdn()
appid = 'e2e0f003-f003-f003-f003-f003f003f003'
app_exe, _ = self.setup_launcher('E2EFaultTrashApp')
config = {
'appid': appid,
'vmin': version,
'abi_hash': abi_hash,
'install_path': self.install_dir,
'local_download_path': self.cdn_dir,
'show_progress_ui': False,
}
if self.test_thumbprint:
config['certificate_thumbprint'] = self.test_thumbprint
installed = self._run_export(app_exe, config, 'install',
'fault_trash_install.json')
self.assertTrue(installed['success'], installed)
deferred = self._run_export(
app_exe, config, 'uninstall', 'fault_trash_uninstall.json',
{'CEF_INSTALLER_TEST_FILE_OPS_FAULT': 'trash_reclaim'})
self.assertTrue(deferred['success'], deferred)
self.assertEqual('cleanup_deferred', deferred['outcome'])
self.assertTrue(deferred.get('warnings'))
self.assert_version_not_installed(version)
trash_dir = os.path.join(self.install_dir, '.trash')
self.assertTrue(os.path.isdir(trash_dir) and os.listdir(trash_dir))
def test_existing_destination_quarantine_failure_and_retry(self):
"""An existing-target collision fails quarantine, then replaces on retry."""
version, abi_hash = self.build_cdn()
appid = 'e2e0f004-f004-f004-f004-f004f004f004'
app_exe, _ = self.setup_launcher('E2EFaultRepairApp')
destination = os.path.join(self.install_dir, 'Versions', version,
'windows64')
os.makedirs(destination, exist_ok=True)
partial = os.path.join(destination, 'partial.txt')
with open(partial, 'w') as f:
f.write('partial')
config = {
'appid': appid,
'vmin': version,
'abi_hash': abi_hash,
'install_path': self.install_dir,
'local_download_path': self.cdn_dir,
'show_progress_ui': False,
}
if self.test_thumbprint:
config['certificate_thumbprint'] = self.test_thumbprint
failed = self._run_export(
app_exe, config, 'install', 'fault_quarantine_failed.json',
{'CEF_INSTALLER_TEST_FILE_OPS_FAULT': 'quarantine_move'})
self.assertFalse(failed['success'])
self.assertEqual(117, failed['error_code'])
self.assertEqual('QUARANTINE_ERROR', failed['error_name'])
self.assertTrue(os.path.isfile(partial))
staging_dir = os.path.join(self.install_dir, '.staging')
self.assertFalse(os.path.isdir(staging_dir) and os.listdir(staging_dir))
retried = self._run_export(app_exe, config, 'install',
'fault_quarantine_retry.json')
self.assertTrue(retried['success'], retried)
self.assertEqual('committed', retried['outcome'])
self.assertFalse(os.path.exists(partial))
self.assert_version_installed(version)
self.assertFalse(os.path.isdir(staging_dir) and os.listdir(staging_dir))
def test_invalid_parent_window_failed_schema(self):
app_exe, _ = self.setup_launcher('E2EParentInvalid')
config = {
'appid': 'e2e0bad0-bad0-bad0-bad0-bad000000000',
'vmin': self.test_version,
'parent_window': 'invalid',
'show_progress_ui': False,
}
marker_path = os.path.join(self.temp_dir, 'parent_marker.json')
env = {
'CEF_E2E_MARKER_PATH': marker_path,
'CEF_E2E_CONFIG_JSON': json.dumps(config),
'CEF_E2E_COMMAND': 'query',
}
self.run_installer(exe=app_exe, use_local_cdn=False, env=env)
with open(marker_path) as f:
result = json.load(f)['result']
self.assertFalse(result['success'])
self.assertEqual('failed', result['outcome'])
self.assertIsInstance(result['error_code'], int)
self.assertEqual('CONFIG_ERROR', result['error_name'])
self.assertIn('parent_window', result['error_message'])
@unittest.skipUnless(sys.maxsize > 2**32, 'requires a 64-bit process')
def test_parent_window_above_uint32_is_accepted(self):
app_exe, _ = self.setup_launcher('E2EParent64Bit')
config = {
'appid': 'e2e06464-6464-6464-6464-646464646464',
'vmin': self.test_version,
'parent_window': str(2**32 + 1),
'show_progress_ui': False,
}
marker_path = os.path.join(self.temp_dir, 'parent_64bit_marker.json')
env = {
'CEF_E2E_MARKER_PATH': marker_path,
'CEF_E2E_CONFIG_JSON': json.dumps(config),
'CEF_E2E_COMMAND': 'query',
}
self.run_installer(exe=app_exe, use_local_cdn=False, env=env)
with open(marker_path) as f:
result = json.load(f)['result']
self.assertNotEqual('CONFIG_ERROR', result.get('error_name'))
self.assertNotIn('parent_window', result.get('error_message', ''))
def test_run_installer_thread_safety(self):
"""RunInstaller thread-local storage is thread-safe."""
version, abi_hash = self.build_cdn()
appid_a = 'e2e00023-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
appid_b = 'e2e00023-bbbb-bbbb-bbbb-bbbbbbbbbbbb'
app_exe, _ = self.setup_launcher('E2E23App')
config_base = {
'vmin': version,
'abi_hash': abi_hash,
'install_path': self.install_dir,
'local_download_path': self.cdn_dir,
'show_progress_ui': False,
}
if self.test_thumbprint:
config_base['certificate_thumbprint'] = self.test_thumbprint
config_a = dict(config_base, appid=appid_a)
config_b = dict(config_base, appid=appid_b)
marker_path = os.path.join(self.temp_dir, 'e2e_marker.json')
env = {
'CEF_E2E_MARKER_PATH': marker_path,
'CEF_E2E_COMMAND': 'install',
'CEF_E2E_THREADING_MODE': 'multi',
'CEF_E2E_CONFIG_JSON_A': json.dumps(config_a),
'CEF_E2E_CONFIG_JSON_B': json.dumps(config_b),
}
exit_code, _, stderr = self.run_installer(args=[],
exe=app_exe,
use_local_cdn=False,
env=env,
timeout=60)
self.assertEqual(
EXIT_SUCCESS, exit_code,
f'Multi-threaded RunInstaller failed (exit {exit_code}): '
f'{stderr.decode(errors="replace")}')
self.assertTrue(os.path.isfile(marker_path), 'Marker file not written')
with open(marker_path) as f:
marker = json.load(f)
result_a = marker.get('result_a', {})
result_b = marker.get('result_b', {})
self.assertTrue(result_a.get('success'),
f'Thread A not successful: {result_a}')
self.assertTrue(result_b.get('success'),
f'Thread B not successful: {result_b}')
# Both apps should be in the database.
db = self.read_database()
uuids = [a.get('uuid') for a in db.get('apps', [])]
self.assertIn(appid_a, uuids, f'App A not in database: {db}')
self.assertIn(appid_b, uuids, f'App B not in database: {db}')
def test_run_installer_parallel(self):
"""RunInstaller is safe to call from multiple threads in parallel."""
version, abi_hash = self.build_cdn()
appid_a = 'e2e00024-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
appid_b = 'e2e00024-bbbb-bbbb-bbbb-bbbbbbbbbbbb'
app_exe, _ = self.setup_launcher('E2E24App')
config_base = {
'vmin': version,
'abi_hash': abi_hash,
'install_path': self.install_dir,
'local_download_path': self.cdn_dir,
'show_progress_ui': False,
}
if self.test_thumbprint:
config_base['certificate_thumbprint'] = self.test_thumbprint
config_a = dict(config_base, appid=appid_a)
config_b = dict(config_base, appid=appid_b)
marker_path = os.path.join(self.temp_dir, 'e2e_marker.json')
env = {
'CEF_E2E_MARKER_PATH': marker_path,
'CEF_E2E_COMMAND': 'install',
'CEF_E2E_THREADING_MODE': 'parallel',
'CEF_E2E_CONFIG_JSON_A': json.dumps(config_a),
'CEF_E2E_CONFIG_JSON_B': json.dumps(config_b),
}
exit_code, _, stderr = self.run_installer(args=[],
exe=app_exe,
use_local_cdn=False,
env=env,
timeout=60)
self.assertEqual(
EXIT_SUCCESS, exit_code,
f'Parallel RunInstaller failed (exit {exit_code}): '
f'{stderr.decode(errors="replace")}')
self.assertTrue(os.path.isfile(marker_path), 'Marker file not written')
with open(marker_path) as f:
marker = json.load(f)
result_a = marker.get('result_a', {})
result_b = marker.get('result_b', {})
self.assertTrue(result_a.get('success'),
f'Thread A not successful: {result_a}')
self.assertTrue(result_b.get('success'),
f'Thread B not successful: {result_b}')
# Both apps should be in the database.
db = self.read_database()
uuids = [a.get('uuid') for a in db.get('apps', [])]
self.assertIn(appid_a, uuids, f'App A not in database: {db}')
self.assertIn(appid_b, uuids, f'App B not in database: {db}')
@@ -0,0 +1,778 @@
#!/usr/bin/env python3
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""E2E tests for the CEF installer uninstall flow."""
import ctypes
import json
import os
import shutil
import subprocess
import tempfile
import time
import unittest
from e2e_test_base import (E2ETestBase, EXIT_CANCELLED, EXIT_CONFIG_ERROR,
EXIT_DATABASE_ERROR, EXIT_INSTALL_ERROR,
EXIT_INDEX_ERROR, EXIT_RELAUNCHED, EXIT_SUCCESS,
FLAG_HEADLESS, FLAG_INSTALL_PATH, FLAG_UNINSTALL,
FLAG_UNINSTALL_RELAUNCHED, FLAG_UPDATE,
FLAG_BACKGROUND, FLAG_PARENT)
from lifecycle_receiver import correlate_helper_output
class TestUninstallFlow(E2ETestBase):
def _stage_default_console_launcher(self, module_name):
"""Stage default-named bootstrapc.exe and a selected mock client DLL."""
host_dir = os.path.join(self.temp_dir, f'host-{module_name}')
os.makedirs(host_dir)
host_exe = os.path.join(host_dir, 'bootstrapc.exe')
module_path = os.path.join(host_dir, f'{module_name}.dll')
shutil.copy2(os.path.join(self.build_dir, 'bootstrapc.exe'), host_exe)
shutil.copy2(os.path.join(self.build_dir, 'chrome_elf.dll'),
os.path.join(host_dir, 'chrome_elf.dll'))
crash_config = os.path.join(self.build_dir, 'crash_reporter.cfg')
if os.path.isfile(crash_config):
shutil.copy2(crash_config, os.path.join(host_dir, 'crash_reporter.cfg'))
shutil.copy2(self.mock_client_dll_src, module_path)
return host_exe, module_path
def _run_default_console_uninstall(self, host_exe, module_name, parent_window,
env, use_install_path_override):
"""Run the async console parent without capturing child-inherited pipes."""
command = [
host_exe, f'--module={module_name}', FLAG_UNINSTALL, FLAG_BACKGROUND,
FLAG_HEADLESS, f'{FLAG_PARENT}={parent_window}'
]
if use_install_path_override:
command.append(f'{FLAG_INSTALL_PATH}={self.install_dir}')
run_env = os.environ.copy()
run_env.update(env)
process = subprocess.run(command,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=30,
cwd=self.build_dir,
env=run_env)
return process.returncode
def _start_lifecycle_receiver(self, *options):
helper = os.path.join(self.build_dir, 'cef_e2e_progress_helper.exe')
if not os.path.isfile(helper):
self.skipTest('cef_e2e_progress_helper.exe not found')
output = os.path.join(self.temp_dir, f'lifecycle-{time.time_ns()}.json')
process = subprocess.Popen([helper, output, '--lifecycle', *options],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=self.build_dir)
line = process.stdout.readline().decode().strip()
self.assertTrue(line, 'No HWND from lifecycle receiver')
return process, int(line), output
def _finish_lifecycle_receiver(self, process, hwnd, output, await_terminal):
try:
if await_terminal:
try:
process.wait(timeout=15)
except subprocess.TimeoutExpired:
pass
if process.poll() is None:
ctypes.windll.user32.PostMessageW(hwnd, 0x0010, 0, 0)
process.wait(timeout=10)
finally:
if process.poll() is None:
process.kill()
process.wait(timeout=10)
self.assertTrue(os.path.isfile(output), 'Receiver output was not written')
with open(output, encoding='utf-8') as output_file:
captured = json.load(output_file)
return captured, correlate_helper_output(captured)
def _install_for_lifecycle(self, appid):
version, abi_hash = self.build_cdn()
self.embed_test_config(appid=appid, vmin=version, abi_hash=abi_hash)
exit_code, _, stderr = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code, stderr.decode(errors='replace'))
return version
def _wait_for_relaunch_child_exit(self, relaunch_dir, timeout=10):
"""Wait for the child image to close before removing its test directory."""
deadline = time.time() + timeout
while time.time() < deadline:
try:
shutil.rmtree(relaunch_dir)
return
except OSError:
time.sleep(0.05)
self.fail(f'Relaunched child did not exit before timeout: {relaunch_dir}')
def _wait_for_path(self, path, description, timeout=10):
deadline = time.time() + timeout
while time.time() < deadline and not os.path.isfile(path):
time.sleep(0.05)
self.assertTrue(os.path.isfile(path), description)
def _run_lifecycle_fault(self, appid, environment=None, receiver_options=()):
version = self._install_for_lifecycle(appid)
staged_exe = self.stage_bootstrap_in_install_root(self.install_dir,
'LifecycleFault')
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
receiver, hwnd, output = self._start_lifecycle_receiver(*receiver_options)
env = {'PATH': self.build_dir + os.pathsep + os.environ.get('PATH', '')}
env.update(environment or {})
exit_code, _, stderr = self.run_installer(
[FLAG_UNINSTALL, FLAG_HEADLESS, f'{FLAG_PARENT}={hwnd}'],
exe=staged_exe,
env=env)
self.assertEqual(EXIT_RELAUNCHED, exit_code,
stderr.decode(errors='replace'))
captured, correlated = self._finish_lifecycle_receiver(receiver,
hwnd,
output,
await_terminal=True)
self.assertEqual(1, len(correlated['handoffs']), captured)
self.assertEqual(1, len(correlated['results']), captured)
operation_id = next(iter(correlated['handoffs']))
self.assertIn(operation_id, correlated['results'])
for path in self.snapshot_uninstall_relaunch_dirs() - relaunch_dirs:
self.addCleanup(shutil.rmtree, path, ignore_errors=True)
return version, captured, correlated['results'][operation_id]
def test_uninstall_removes_app(self):
"""External uninstall is synchronous and prunes the version."""
version, abi_hash = self.build_cdn()
appid = 'e2e00008-0008-0008-0008-000000000008'
self.embed_test_config(appid=appid, vmin=version, abi_hash=abi_hash)
exit_code, _, stderr = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code,
f'Install failed: {stderr.decode(errors="replace")}')
self.assert_version_installed(version)
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
exit_code, _, stderr = self.run_installer([FLAG_UNINSTALL, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code,
f'Uninstall failed: {stderr.decode(errors="replace")}')
self.assertEqual(relaunch_dirs, self.snapshot_uninstall_relaunch_dirs())
self.assert_version_not_installed(version)
self.assertFalse(self._database_contains_app(self.install_dir, appid))
def test_uninstall_with_other_apps(self):
"""Synchronous uninstall preserves another app's version."""
version, abi_hash = self.build_cdn()
appid_a = 'e2e00009-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
appid_b = 'e2e00009-bbbb-bbbb-bbbb-bbbbbbbbbbbb'
self.embed_test_config(appid=appid_a, vmin=version, abi_hash=abi_hash)
exit_code, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code)
self.embed_test_config(appid=appid_b, vmin=version, abi_hash=abi_hash)
exit_code, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code)
self.embed_test_config(appid=appid_a, vmin=version, abi_hash=abi_hash)
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
exit_code, _, _ = self.run_installer([FLAG_UNINSTALL, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code)
self.assertEqual(relaunch_dirs, self.snapshot_uninstall_relaunch_dirs())
self.assert_version_installed(version)
db = self.read_database()
uuids = [app.get('uuid') for app in db.get('apps', [])]
self.assertNotIn(appid_a, uuids)
self.assertIn(appid_b, uuids)
def test_uninstall_contained_executable_relocation(self):
"""An executable inside the writable target returns exactly 109."""
version, abi_hash = self.build_cdn()
appid = 'e2e00010-0010-0010-0010-000000000010'
self.embed_test_config(appid=appid, vmin=version, abi_hash=abi_hash)
exit_code, _, stderr = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code,
f'Install failed: {stderr.decode(errors="replace")}')
self.assert_version_installed(version)
staged_exe = self.stage_bootstrap_in_install_root(self.install_dir)
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
env = {'PATH': self.build_dir + os.pathsep + os.environ.get('PATH', '')}
exit_code, _, stderr = self.run_installer([FLAG_UNINSTALL, FLAG_HEADLESS],
exe=staged_exe,
env=env)
self.assertEqual(
EXIT_RELAUNCHED, exit_code, f'Unexpected exit code {exit_code}: '
f'{stderr.decode(errors="replace")}')
self.wait_for_relaunched_uninstall(appid,
relaunch_dirs,
os.path.basename(staged_exe),
expected_index_versions=())
self.assert_version_not_installed(version)
self.assertFalse(self._database_contains_app(self.install_dir, appid))
def test_uninstall_required_relaunch_failure_is_105(self):
"""A required relaunch failure does not fall back to direct mutation."""
version, abi_hash = self.build_cdn()
appid = 'e2e00010-0105-0105-0105-000000000105'
self.embed_test_config(appid=appid, vmin=version, abi_hash=abi_hash)
exit_code, _, stderr = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code, stderr.decode(errors='replace'))
staged_exe = self.stage_bootstrap_in_install_root(
self.install_dir, 'FailedContainedUninstall')
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
env = {
'PATH': self.build_dir + os.pathsep + os.environ.get('PATH', ''),
'CEF_INSTALLER_TEST_RELAUNCH_FAILURE': '1',
}
exit_code, _, stderr = self.run_installer([FLAG_UNINSTALL, FLAG_HEADLESS],
exe=staged_exe,
env=env)
self.assertEqual(EXIT_INSTALL_ERROR, exit_code,
stderr.decode(errors='replace'))
self.assertEqual(relaunch_dirs, self.snapshot_uninstall_relaunch_dirs())
self.assertTrue(self._database_contains_app(self.install_dir, appid))
self.assert_version_installed(version)
def test_uninstall_marker_only_is_rejected(self):
"""The internal marker cannot bypass trusted relaunch state."""
version, abi_hash = self.build_cdn()
appid = 'e2e00010-0100-0100-0100-000000000100'
self.embed_test_config(appid=appid, vmin=version, abi_hash=abi_hash)
exit_code, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code)
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
exit_code, _, _ = self.run_installer(
[FLAG_UNINSTALL, FLAG_HEADLESS, FLAG_UNINSTALL_RELAUNCHED])
self.assertEqual(EXIT_CONFIG_ERROR, exit_code)
self.assertEqual(relaunch_dirs, self.snapshot_uninstall_relaunch_dirs())
self.assertTrue(self._database_contains_app(self.install_dir, appid))
self.assert_version_installed(version)
def test_uninstall_state_on_update_is_rejected(self):
"""Relaunch marker/state transport is invalid on every other command."""
version, abi_hash = self.build_cdn()
appid = 'e2e00010-0200-0200-0200-000000000200'
self.embed_test_config(appid=appid, vmin=version, abi_hash=abi_hash)
exit_code, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code)
relaunch_dir = tempfile.mkdtemp(prefix='cef_uninstall_')
self.addCleanup(shutil.rmtree, relaunch_dir, ignore_errors=True)
staged_exe = self.stage_bootstrap_in_install_root(relaunch_dir,
'StateOnUpdate')
nonce = 'valid-state-on-update'
with open(os.path.join(relaunch_dir, 'cef_uninstall_state.json'),
'w',
encoding='utf-8') as state_file:
json.dump({'install_path': self.install_dir, 'nonce': nonce}, state_file)
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
env = {'PATH': self.build_dir + os.pathsep + os.environ.get('PATH', '')}
exit_code, _, _ = self.run_installer([
FLAG_UPDATE, FLAG_HEADLESS, FLAG_UNINSTALL_RELAUNCHED,
f'/cef-uninstall-state={nonce}'
],
exe=staged_exe,
env=env)
self.assertEqual(EXIT_CONFIG_ERROR, exit_code)
self.assertEqual(relaunch_dirs, self.snapshot_uninstall_relaunch_dirs())
self.assertTrue(self._database_contains_app(self.install_dir, appid))
self.assert_version_installed(version)
def test_uninstall_invalid_exclusive_custom_has_no_relaunch(self):
"""An invalid exclusive target fails normally without creating a child."""
version, abi_hash = self.build_cdn()
self.embed_test_config(appid='e2e00010-0300-0300-0300-000000000300',
vmin=version,
abi_hash=abi_hash)
invalid_target = os.path.join(self.temp_dir, 'not-a-directory')
with open(invalid_target, 'w') as invalid_file:
invalid_file.write('file')
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
exit_code, _, _ = self.run_installer([FLAG_UNINSTALL, FLAG_HEADLESS],
install_path=invalid_target)
self.assertEqual(EXIT_CONFIG_ERROR, exit_code)
self.assertEqual(relaunch_dirs, self.snapshot_uninstall_relaunch_dirs())
def test_uninstall_read_only_exclusive_custom_has_no_relaunch(self):
"""A real read-only exclusive custom store fails without a child."""
version, abi_hash = self.build_cdn()
appid = 'e2e00010-0301-0301-0301-000000000301'
self.embed_test_config(appid=appid, vmin=version, abi_hash=abi_hash)
exit_code, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code)
self.assertTrue(self._database_contains_app(self.install_dir, appid))
self.assert_version_installed(version)
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
with self.scoped_read_only_directory(self.install_dir):
exit_code, _, _ = self.run_installer([FLAG_UNINSTALL, FLAG_HEADLESS])
self.assertEqual(EXIT_CONFIG_ERROR, exit_code)
self.assertEqual(relaunch_dirs, self.snapshot_uninstall_relaunch_dirs())
self.assertTrue(self._database_contains_app(self.install_dir, appid))
self.assert_version_installed(version)
def test_uninstall_mutation_ineligible_admin_has_no_relaunch(self):
"""An elevated admin role denied mutation never falls through per-user."""
version, abi_hash = self.build_cdn()
appid = 'e2e00010-0302-0302-0302-000000000302'
self.embed_test_config(appid=appid, vmin=version, abi_hash=abi_hash)
role_root = os.path.join(self.temp_dir, 'role-root')
admin_store = os.path.join(role_root, 'Admin')
per_user_store = os.path.join(role_root, 'PerUser')
os.makedirs(per_user_store)
exit_code, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS],
install_path=admin_store)
self.assertEqual(EXIT_SUCCESS, exit_code)
self.assertTrue(self._database_contains_app(admin_store, appid))
version_path = os.path.join(admin_store, 'Versions', version, 'windows64')
self.assertTrue(os.path.isdir(version_path))
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
env = {
'CEF_INSTALLER_TEST_DIRECTORY_SCENARIO': 'admin_mutation_denied',
'CEF_INSTALLER_TEST_DIRECTORY_ROOT': role_root,
}
exit_code, _, _ = self.run_installer([FLAG_UNINSTALL, FLAG_HEADLESS],
use_install_path_override=False,
env=env)
self.assertEqual(EXIT_CONFIG_ERROR, exit_code)
self.assertEqual(relaunch_dirs, self.snapshot_uninstall_relaunch_dirs())
self.assertTrue(self._database_contains_app(admin_store, appid))
self.assertTrue(os.path.isdir(version_path))
self.assertEqual([], os.listdir(per_user_store))
def test_uninstall_database_failure_is_synchronous(self):
"""An external caller receives the exact controller database failure."""
version, abi_hash = self.build_cdn()
appid = 'e2e00010-0106-0106-0106-000000000106'
self.embed_test_config(appid=appid, vmin=version, abi_hash=abi_hash)
exit_code, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code)
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
exit_code, _, _ = self.run_installer(
[FLAG_UNINSTALL, FLAG_HEADLESS],
env={'CEF_INSTALLER_TEST_DATABASE_SAVE_FAILURE': '1'})
self.assertEqual(EXIT_DATABASE_ERROR, exit_code)
self.assertEqual(relaunch_dirs, self.snapshot_uninstall_relaunch_dirs())
self.assertTrue(self._database_contains_app(self.install_dir, appid))
self.assert_version_installed(version)
def test_uninstall_index_failure_is_synchronous(self):
"""An external caller receives the exact checked-index failure."""
version, abi_hash = self.build_cdn()
appid = 'e2e00010-0114-0114-0114-000000000114'
self.embed_test_config(appid=appid, vmin=version, abi_hash=abi_hash)
exit_code, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code)
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
exit_code, _, _ = self.run_installer(
[FLAG_UNINSTALL, FLAG_HEADLESS],
env={'CEF_INSTALLER_TEST_INDEX_FAULT': 'write'})
self.assertEqual(EXIT_INDEX_ERROR, exit_code)
self.assertEqual(relaunch_dirs, self.snapshot_uninstall_relaunch_dirs())
self.assertFalse(self._database_contains_app(self.install_dir, appid))
self.assert_version_installed(version)
def test_uninstall_cleanup_failure_is_synchronous_success(self):
"""Post-commit physical cleanup failure remains exact exit zero."""
version, abi_hash = self.build_cdn()
appid = 'e2e00010-0000-0000-0000-000000000109'
self.embed_test_config(appid=appid, vmin=version, abi_hash=abi_hash)
exit_code, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code)
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
exit_code, _, _ = self.run_installer(
[FLAG_UNINSTALL, FLAG_HEADLESS],
env={'CEF_INSTALLER_TEST_FILE_OPS_FAULT': 'trash_move'})
self.assertEqual(EXIT_SUCCESS, exit_code)
self.assertEqual(relaunch_dirs, self.snapshot_uninstall_relaunch_dirs())
self.assertFalse(self._database_contains_app(self.install_dir, appid))
self.assert_version_installed(version)
def test_lifecycle_contained_uninstall_correlates_terminal(self):
appid = 'e2e00010-1001-1001-1001-000000001001'
version = self._install_for_lifecycle(appid)
staged_exe = self.stage_bootstrap_in_install_root(self.install_dir,
'LifecycleContained')
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
receiver, hwnd, output = self._start_lifecycle_receiver()
env = {'PATH': self.build_dir + os.pathsep + os.environ.get('PATH', '')}
exit_code, _, stderr = self.run_installer(
[FLAG_UNINSTALL, FLAG_HEADLESS, f'{FLAG_PARENT}={hwnd}'],
exe=staged_exe,
env=env)
self.assertEqual(EXIT_RELAUNCHED, exit_code,
stderr.decode(errors='replace'))
captured, correlated = self._finish_lifecycle_receiver(receiver,
hwnd,
output,
await_terminal=True)
self.assertEqual(0, captured['rejected'])
self.assertEqual(1, len(correlated['handoffs']),
f'captured={captured!r}, correlated={correlated!r}')
self.assertEqual(1, len(correlated['results']))
operation_id = next(iter(correlated['handoffs']))
self.assertIn(operation_id, correlated['results'])
result = correlated['results'][operation_id]
self.assertTrue(result['success'])
self.assertEqual('committed', result['outcome'])
self.assertEqual(0, result['exit_code'])
serialized = json.dumps(result)
self.assertNotIn(self.temp_dir, serialized)
self.assertNotIn('nonce', serialized.lower())
self.wait_for_relaunched_uninstall(appid, relaunch_dirs,
os.path.basename(staged_exe), ())
self.assert_version_not_installed(version)
def test_lifecycle_external_uninstall_remains_synchronous(self):
appid = 'e2e00010-1011-1011-1011-000000001011'
self._install_for_lifecycle(appid)
receiver, hwnd, output = self._start_lifecycle_receiver()
exit_code, _, stderr = self.run_installer(
[FLAG_UNINSTALL, FLAG_HEADLESS, f'{FLAG_PARENT}={hwnd}'])
self.assertEqual(EXIT_SUCCESS, exit_code, stderr.decode(errors='replace'))
captured, correlated = self._finish_lifecycle_receiver(receiver,
hwnd,
output,
await_terminal=False)
self.assertGreater(len(captured['progress']), 0)
self.assertEqual([], captured['lifecycle'])
self.assertEqual({}, correlated['handoffs'])
self.assertEqual({}, correlated['results'])
def test_lifecycle_no_parent_has_no_bound_context(self):
appid = 'e2e00010-1012-1012-1012-000000001012'
self._install_for_lifecycle(appid)
staged_exe = self.stage_bootstrap_in_install_root(self.install_dir,
'LifecycleNoParent')
barrier = os.path.join(self.temp_dir, 'no-parent-child-state-ready')
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
env = {
'PATH': self.build_dir + os.pathsep + os.environ.get('PATH', ''),
'CEF_INSTALLER_TEST_CHILD_STATE_BARRIER': barrier,
}
exit_code, _, _ = self.run_installer([FLAG_UNINSTALL, FLAG_HEADLESS],
exe=staged_exe,
env=env)
self.assertEqual(EXIT_RELAUNCHED, exit_code)
self._wait_for_path(barrier, 'Child did not reach trusted-state barrier')
candidates = self.snapshot_uninstall_relaunch_dirs() - relaunch_dirs
self.assertEqual(1, len(candidates))
with open(os.path.join(next(iter(candidates)), 'cef_uninstall_state.json'),
encoding='utf-8') as state_file:
state = json.load(state_file)
self.assertNotIn('operation_id', state)
self.assertNotIn('parent_window', state)
os.remove(barrier)
for path in candidates:
self._wait_for_relaunch_child_exit(path)
def test_lifecycle_invalid_trusted_invocation_emits_nothing(self):
appid = 'e2e00010-1013-1013-1013-000000001013'
self._install_for_lifecycle(appid)
receiver, hwnd, output = self._start_lifecycle_receiver()
exit_code, _, _ = self.run_installer([
FLAG_UNINSTALL, FLAG_HEADLESS, FLAG_UNINSTALL_RELAUNCHED,
f'{FLAG_PARENT}={hwnd}'
])
self.assertEqual(EXIT_CONFIG_ERROR, exit_code)
captured, correlated = self._finish_lifecycle_receiver(receiver,
hwnd,
output,
await_terminal=False)
self.assertEqual([], captured['lifecycle'])
self.assertEqual({}, correlated['handoffs'])
self.assertEqual({}, correlated['results'])
def test_lifecycle_background_uninstall_is_headless_and_correlated(self):
appid = 'e2e00010-1002-1002-1002-000000001002'
self._install_for_lifecycle(appid)
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
receiver, hwnd, output = self._start_lifecycle_receiver(
'--lifecycle-return=2')
env = {'PATH': self.build_dir + os.pathsep + os.environ.get('PATH', '')}
exit_code, _, stderr = self.run_installer([
FLAG_UNINSTALL, FLAG_HEADLESS, FLAG_BACKGROUND, f'{FLAG_PARENT}={hwnd}'
],
env=env)
self.assertEqual(EXIT_RELAUNCHED, exit_code,
stderr.decode(errors='replace'))
captured, correlated = self._finish_lifecycle_receiver(receiver,
hwnd,
output,
await_terminal=True)
self.assertEqual(1, len(correlated['results']),
f'captured={captured!r}, correlated={correlated!r}')
self.assertTrue(next(iter(correlated['results'].values()))['success'])
self.wait_for_relaunched_uninstall(appid, relaunch_dirs,
os.path.basename(self.bootstrap_exe), ())
def test_selected_client_resource_relaunch_preserves_module_selection(self):
appid = 'e2e00010-1101-1101-1101-000000001101'
fallback_appid = 'e2e00010-1101-1101-1101-000000001199'
version = self._install_for_lifecycle(appid)
self.assertTrue(self._database_contains_app(self.install_dir, appid))
self.assertFalse(
self._database_contains_app(self.install_dir, fallback_appid))
host_exe, module_path = self._stage_default_console_launcher(
'SelectedRelaunchClient')
self.embed_test_config(appid=fallback_appid,
vmin=version,
exe=host_exe,
enable_explicit_modes=True)
client_config = {
'appid': appid,
'vmin': version,
'install_path': self.install_dir,
}
if self.test_thumbprint:
client_config['certificate_thumbprint'] = self.test_thumbprint
self.embed_config_resource(module_path, client_config)
barrier = os.path.join(self.temp_dir, 'selected-client-child-ready')
marker = os.path.join(self.temp_dir, 'unexpected-client-marker.json')
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
receiver, hwnd, output = self._start_lifecycle_receiver()
env = {
'PATH': self.build_dir + os.pathsep + os.environ.get('PATH', ''),
'CEF_INSTALLER_TEST_CHILD_STATE_BARRIER': barrier,
'CEF_E2E_MARKER_PATH': marker,
}
exit_code = self._run_default_console_uninstall(
host_exe,
'SelectedRelaunchClient',
hwnd,
env,
use_install_path_override=False)
self.assertEqual(EXIT_RELAUNCHED, exit_code)
self._wait_for_path(barrier, 'Selected-client child did not accept state')
candidates = self.snapshot_uninstall_relaunch_dirs() - relaunch_dirs
self.assertEqual(1, len(candidates))
relaunch_dir = next(iter(candidates))
relaunch_files = os.listdir(relaunch_dir)
self.assertTrue(
os.path.isfile(os.path.join(relaunch_dir,
'SelectedRelaunchClient.dll')),
f'Relaunch file set: {relaunch_files}')
with open(os.path.join(relaunch_dir, 'cef_uninstall_state.json'),
encoding='utf-8') as state_file:
state = json.load(state_file)
self.assertEqual(os.path.normcase(os.path.abspath(self.install_dir)),
os.path.normcase(os.path.abspath(state['install_path'])))
self.assertNotIn('unchecked_cef_path', state)
self.assertNotIn('bundled_cef_path', state)
self.assertNotIn('config', state)
os.remove(barrier)
_, correlated = self._finish_lifecycle_receiver(receiver,
hwnd,
output,
await_terminal=True)
result = next(iter(correlated['results'].values()))
self.assertTrue(result['success'])
self.assertFalse(os.path.exists(marker),
'Temporary child entered normal client execution')
self.wait_for_relaunched_uninstall(appid, relaunch_dirs, 'bootstrapc.exe',
())
self.assertFalse(self._database_contains_app(self.install_dir, appid))
def test_bootstrap_fallback_relaunch_omits_no_resource_client(self):
appid = 'e2e00010-1102-1102-1102-000000001102'
version = self._install_for_lifecycle(appid)
host_exe, _ = self._stage_default_console_launcher('NoResourceClient')
self.embed_test_config(appid=appid, vmin=version, exe=host_exe)
barrier = os.path.join(self.temp_dir, 'bootstrap-fallback-child-ready')
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
receiver, hwnd, output = self._start_lifecycle_receiver()
env = {
'PATH': self.build_dir + os.pathsep + os.environ.get('PATH', ''),
'CEF_INSTALLER_TEST_CHILD_STATE_BARRIER': barrier,
}
exit_code = self._run_default_console_uninstall(
host_exe, 'NoResourceClient', hwnd, env, use_install_path_override=True)
self.assertEqual(EXIT_RELAUNCHED, exit_code)
self._wait_for_path(barrier,
'Bootstrap-fallback child did not accept state')
candidates = self.snapshot_uninstall_relaunch_dirs() - relaunch_dirs
self.assertEqual(1, len(candidates))
relaunch_dir = next(iter(candidates))
self.assertFalse(
os.path.exists(os.path.join(relaunch_dir, 'NoResourceClient.dll')))
os.remove(barrier)
_, correlated = self._finish_lifecycle_receiver(receiver,
hwnd,
output,
await_terminal=True)
self.assertTrue(next(iter(correlated['results'].values()))['success'])
self.wait_for_relaunched_uninstall(appid, relaunch_dirs, 'bootstrapc.exe',
())
def test_lifecycle_child_config_failure_reports_terminal(self):
appid = 'e2e00010-1003-1003-1003-000000001003'
self._install_for_lifecycle(appid)
staged_exe = self.stage_bootstrap_in_install_root(self.install_dir,
'LifecycleConfigFailure')
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
receiver, hwnd, output = self._start_lifecycle_receiver()
env = {
'PATH': self.build_dir + os.pathsep + os.environ.get('PATH', ''),
'CEF_INSTALLER_TEST_CHILD_CONFIG_FAILURE': '1',
}
exit_code, _, stderr = self.run_installer(
[FLAG_UNINSTALL, FLAG_HEADLESS, f'{FLAG_PARENT}={hwnd}'],
exe=staged_exe,
env=env)
self.assertEqual(EXIT_RELAUNCHED, exit_code,
stderr.decode(errors='replace'))
_, correlated = self._finish_lifecycle_receiver(receiver,
hwnd,
output,
await_terminal=True)
result = next(iter(correlated['results'].values()))
self.assertFalse(result['success'])
self.assertEqual(EXIT_CONFIG_ERROR, result['exit_code'])
self.assertEqual(EXIT_CONFIG_ERROR, result['error_code'])
self.assertTrue(self._database_contains_app(self.install_dir, appid))
for path in self.snapshot_uninstall_relaunch_dirs() - relaunch_dirs:
self.addCleanup(shutil.rmtree, path, ignore_errors=True)
def test_lifecycle_relaunch_failure_emits_no_events(self):
appid = 'e2e00010-1004-1004-1004-000000001004'
self._install_for_lifecycle(appid)
staged_exe = self.stage_bootstrap_in_install_root(self.install_dir,
'LifecycleLaunchFailure')
receiver, hwnd, output = self._start_lifecycle_receiver()
env = {
'PATH': self.build_dir + os.pathsep + os.environ.get('PATH', ''),
'CEF_INSTALLER_TEST_RELAUNCH_FAILURE': '1',
}
exit_code, _, _ = self.run_installer(
[FLAG_UNINSTALL, FLAG_HEADLESS, f'{FLAG_PARENT}={hwnd}'],
exe=staged_exe,
env=env)
self.assertEqual(EXIT_INSTALL_ERROR, exit_code)
captured, correlated = self._finish_lifecycle_receiver(receiver,
hwnd,
output,
await_terminal=False)
self.assertEqual([], captured['lifecycle'])
self.assertEqual({}, correlated['handoffs'])
self.assertEqual({}, correlated['results'])
def test_lifecycle_database_failure_reports_exact_terminal(self):
appid = 'e2e00010-1007-1007-1007-000000001007'
version, captured, result = self._run_lifecycle_fault(
appid, {'CEF_INSTALLER_TEST_DATABASE_SAVE_FAILURE': '1'})
self.assertFalse(result['success'])
self.assertEqual('failed', result['outcome'])
self.assertEqual(EXIT_DATABASE_ERROR, result['exit_code'])
self.assertEqual(EXIT_DATABASE_ERROR, result['error_code'])
self.assertNotIn('retry_required', result)
self.assertNotIn(self.temp_dir, json.dumps(captured))
self.assertTrue(self._database_contains_app(self.install_dir, appid))
self.assert_version_installed(version)
def test_lifecycle_index_failure_does_not_claim_rollback(self):
appid = 'e2e00010-1008-1008-1008-000000001008'
version, captured, result = self._run_lifecycle_fault(
appid, {'CEF_INSTALLER_TEST_INDEX_FAULT': 'write'})
self.assertFalse(result['success'])
self.assertEqual(EXIT_INDEX_ERROR, result['exit_code'])
self.assertEqual(EXIT_INDEX_ERROR, result['error_code'])
self.assertNotIn('registrations_committed', result)
self.assertNotIn('rollback', json.dumps(result).lower())
self.assertNotIn(self.temp_dir, json.dumps(captured))
self.assertFalse(self._database_contains_app(self.install_dir, appid))
self.assert_version_installed(version)
def test_lifecycle_deferred_cleanup_is_success_with_warning(self):
appid = 'e2e00010-1009-1009-1009-000000001009'
version, captured, result = self._run_lifecycle_fault(
appid, {'CEF_INSTALLER_TEST_FILE_OPS_FAULT': 'trash_move'})
self.assertTrue(result['success'])
self.assertEqual('cleanup_deferred', result['outcome'])
self.assertEqual(EXIT_SUCCESS, result['exit_code'])
self.assertGreater(len(result.get('warnings', [])), 0)
self.assertNotIn(self.temp_dir, json.dumps(captured))
self.assertFalse(self._database_contains_app(self.install_dir, appid))
self.assert_version_installed(version)
def test_lifecycle_progress_cancellation_reports_exact_terminal(self):
appid = 'e2e00010-1010-1010-1010-000000001010'
version, captured, result = self._run_lifecycle_fault(
appid, receiver_options=('--cancel',))
self.assertGreater(len(captured['progress']), 0)
self.assertFalse(result['success'])
self.assertEqual('failed', result['outcome'])
self.assertEqual(EXIT_CANCELLED, result['exit_code'])
self.assertEqual(EXIT_CANCELLED, result['error_code'])
self.assertTrue(self._database_contains_app(self.install_dir, appid))
self.assert_version_installed(version)
def test_lifecycle_forced_child_termination_is_indeterminate(self):
appid = 'e2e00010-1006-1006-1006-000000001006'
self._install_for_lifecycle(appid)
staged_exe = self.stage_bootstrap_in_install_root(
self.install_dir, 'LifecycleForcedTermination')
barrier = os.path.join(self.temp_dir, 'child-state-ready')
relaunch_dirs = self.snapshot_uninstall_relaunch_dirs()
receiver, hwnd, output = self._start_lifecycle_receiver(
'--close-after-handoff')
env = {
'PATH': self.build_dir + os.pathsep + os.environ.get('PATH', ''),
'CEF_INSTALLER_TEST_CHILD_STATE_BARRIER': barrier,
}
exit_code, _, _ = self.run_installer(
[FLAG_UNINSTALL, FLAG_HEADLESS, f'{FLAG_PARENT}={hwnd}'],
exe=staged_exe,
env=env)
self.assertEqual(EXIT_RELAUNCHED, exit_code)
captured, correlated = self._finish_lifecycle_receiver(receiver,
hwnd,
output,
await_terminal=True)
self.assertEqual(1, len(captured['lifecycle']))
self.assertEqual({}, correlated['results'])
self._wait_for_path(barrier, 'Child did not reach state barrier')
child_pid = int(next(iter(correlated['handoffs'].values()))['child_pid'])
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
kernel32.OpenProcess.restype = ctypes.c_void_p
kernel32.OpenProcess.argtypes = [
ctypes.c_uint32, ctypes.c_int, ctypes.c_uint32
]
kernel32.TerminateProcess.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
kernel32.WaitForSingleObject.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
kernel32.CloseHandle.argtypes = [ctypes.c_void_p]
process_handle = kernel32.OpenProcess(0x00100001, False, child_pid)
self.assertTrue(process_handle, 'Failed to open relaunched child')
try:
self.assertTrue(kernel32.TerminateProcess(process_handle, 108))
self.assertEqual(0, kernel32.WaitForSingleObject(process_handle, 10000))
finally:
kernel32.CloseHandle(process_handle)
os.remove(barrier)
self.assertTrue(self._database_contains_app(self.install_dir, appid))
for path in self.snapshot_uninstall_relaunch_dirs() - relaunch_dirs:
self.addCleanup(shutil.rmtree, path, ignore_errors=True)
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
# Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""E2E tests for the CEF installer update flow."""
import unittest
from e2e_test_base import (E2ETestBase, EXIT_SUCCESS, FLAG_BACKGROUND,
FLAG_FORCECHECK, FLAG_HEADLESS, FLAG_UPDATE)
class TestUpdateFlow(E2ETestBase):
def test_update_finds_newer_version(self):
"""Update downloads newer version when vmin moves forward."""
abi_hash = 'a1b2c3d4e5f6'
appid = 'e2e00005-0005-0005-0005-000000000005'
# Build CDN with initial version and install.
v1 = self.test_version
v1_1 = self.test_version_update
self.build_cdn(version=v1, abi_hash=abi_hash)
self.embed_test_config(appid=appid, vmin=v1, abi_hash=abi_hash)
exit_code, _, stderr = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(
EXIT_SUCCESS, exit_code,
f'Initial install failed: {stderr.decode(errors="replace")}')
self.assert_version_installed(v1)
# Add newer version to CDN (manifest merges).
self.build_cdn(version=v1_1, abi_hash=abi_hash)
# Raise vmin so the controller downloads the newer version.
self.embed_test_config(appid=appid, vmin=v1_1, abi_hash=abi_hash)
exit_code, _, stderr = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code,
f'Update failed: {stderr.decode(errors="replace")}')
# Newer version installed, old version pruned.
self.assert_version_installed(v1_1)
self.assert_version_not_installed(v1)
db = self.read_database()
found = any(a.get('uuid') == appid for a in db.get('apps', []))
self.assertTrue(found, f'App not in database: {db}')
def test_update_with_forcecheck(self):
"""Update with /cef-forcecheck bypasses manifest cache."""
version, abi_hash = self.build_cdn()
self.embed_test_config(
appid='e2e00006-0006-0006-0006-000000000006',
vmin=version,
abi_hash=abi_hash,
)
# Install.
exit_code, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code)
# Normal update (may use cached manifest).
exit_code, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code)
# Forced update bypasses cache.
exit_code, _, _ = self.run_installer(
[FLAG_UPDATE, FLAG_HEADLESS, FLAG_FORCECHECK])
self.assertEqual(EXIT_SUCCESS, exit_code)
self.assert_version_installed(version)
def test_explicit_background_update(self):
"""Explicit background update completes without UI."""
version, abi_hash = self.build_cdn()
self.embed_test_config(
appid='e2e00031-0031-0031-0031-000000000031',
vmin=version,
abi_hash=abi_hash,
)
exit_code, _, stderr = self.run_installer([FLAG_UPDATE, FLAG_BACKGROUND])
self.assertEqual(
EXIT_SUCCESS, exit_code,
f'Background update failed: {stderr.decode(errors="replace")}')
self.assert_version_installed(version)
def test_update_no_newer_version(self):
"""Update with no newer version available is a no-op success."""
version, abi_hash = self.build_cdn()
self.embed_test_config(
appid='e2e00007-0007-0007-0007-000000000007',
vmin=version,
abi_hash=abi_hash,
)
# Install.
exit_code, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code)
self.assert_version_installed(version)
# Second update -- already at latest, should succeed without changes.
exit_code, _, _ = self.run_installer([FLAG_UPDATE, FLAG_HEADLESS])
self.assertEqual(EXIT_SUCCESS, exit_code)
self.assert_version_installed(version)
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,223 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_ARCHIVE_H_
#define CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_ARCHIVE_H_
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/functional/callback.h"
#include "base/time/time.h"
namespace cef_installer {
// Security limits to prevent resource exhaustion attacks.
constexpr uint64_t kMaxFileSize = 4ULL * 1024 * 1024 * 1024; // 4 GB
constexpr uint64_t kMaxTotalExtractionSize =
10ULL * 1024 * 1024 * 1024; // 10 GB
constexpr size_t kMaxPathLength = 4096;
constexpr size_t kMaxEntryCount = 100000;
// Error codes for archive operations.
enum class ArchiveError {
kSuccess,
kFileNotFound, // Archive file doesn't exist
kFileReadError, // Could not read archive
kInvalidFormat, // Not a valid tar.xz file (xz decompression failed)
kInvalidHeader, // Tar header checksum mismatch or invalid
kExtractionFailed, // General extraction failure
kDiskFull, // Not enough space to extract
kPathTraversal, // Archive contains path traversal attack (../)
kAbsolutePath, // Archive contains absolute path
kWriteError, // Could not write extracted file
kUnsupportedEntryType, // Entry type not supported (e.g., device nodes)
kCancelled, // Extraction cancelled via callback
};
// Callback for extraction progress reporting.
// Parameters: bytes_processed, total_bytes
// Return false to cancel extraction.
using ExtractionProgressCallback =
base::RepeatingCallback<bool(uint64_t, uint64_t)>;
// Configuration for extraction (single struct for both paths).
struct ExtractionConfig {
// Background mode: minimizes system impact for non-interactive extraction
// (e.g. scheduled updates). Always uses the streaming single-threaded path
// (~128 KB memory) rather than the parallel path (~500 MB allocation).
bool background_mode = false;
// Max threads (0 = auto-detect from CPU count).
// Only used when parallel extraction is possible (multi-block archive).
int max_threads = 0;
// Max memory per thread for decompression buffers (default 64MB).
// Covers most LZMA2 dictionary sizes.
size_t max_memory_per_thread = 64 * 1024 * 1024;
};
// Primary API - auto-selects parallel vs single-threaded based on archive.
// For multi-block XZ archives, uses parallel block decompression.
// For single-block archives, falls back to streaming single-threaded path.
//
// - Creates dest_dir if it doesn't exist
// - Extracts all files and directories preserving structure
// - Validates no path traversal (rejects entries with ".." components)
// - Validates no absolute paths (rejects entries starting with "/" or "C:\")
// - Preserves modification times
// - Archive format: xz/LZMA2-compressed tar (POSIX ustar format)
//
// Uses //third_party/lzma_sdk/src/C/Xz.h for xz decompression.
ArchiveError ExtractTarXz(const base::FilePath& archive_path,
const base::FilePath& dest_dir,
const ExtractionConfig& config = {},
ExtractionProgressCallback progress = {});
// Explicit single-threaded streaming extraction (for fallback).
// Also used by installer_parallel_xz.cc as fallback.
ArchiveError ExtractTarXzSingleThread(const base::FilePath& archive_path,
const base::FilePath& dest_dir,
ExtractionProgressCallback progress = {});
// Convert error code to human-readable string for logging.
const char* ArchiveErrorToString(ArchiveError error);
// Decompress a single XZ block (not a full XZ stream).
// |block_data| must start at the block header.
// |check_type| is the stream check type (0=none, 1=CRC32, 4=CRC64, 10=SHA256).
// |stream_flags| is the raw CXzStreamFlags value from the stream header/footer.
// |output| must be pre-sized to the expected uncompressed size (from XZ index).
// Returns true on success.
bool DecompressXzBlock(base::span<const uint8_t> block_data,
uint8_t check_type,
uint16_t stream_flags,
base::span<uint8_t> output);
// Extract tar archive from an in-memory buffer.
// Processes tar headers and extracts files to |dest_dir|.
ArchiveError ExtractTarFromBuffer(base::span<const uint8_t> tar_data,
const base::FilePath& dest_dir,
ExtractionProgressCallback progress = {});
// ============================================================================
// Internal helpers exposed for testing. Not part of the public API.
// ============================================================================
namespace internal {
// Test observation for the most recent top-level extraction request.
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
void ResetLastExtractionBackgroundModeForTesting();
std::optional<bool> GetLastExtractionBackgroundModeForTesting();
#endif
// Information about a single entry in a tar archive.
struct TarEntry {
enum class Type {
kRegularFile,
kDirectory,
kOther, // Symlinks, hard links, devices, etc. - skipped
};
base::FilePath path; // Relative path within archive
uint64_t size = 0; // File size in bytes
base::Time mtime; // Last modification time
Type type = Type::kOther;
};
// TarReader parses tar headers and validates entries.
// Reference: chrome/services/file_util/single_file_tar_reader.cc
class TarReader {
public:
// Tar header constants (POSIX ustar format)
// https://www.gnu.org/software/tar/manual/html_node/Standard.html
static constexpr size_t kHeaderSize = 512;
static constexpr size_t kNameOffset = 0;
static constexpr size_t kNameLength = 100;
static constexpr size_t kModeOffset = 100;
static constexpr size_t kSizeOffset = 124;
static constexpr size_t kSizeLength = 12;
static constexpr size_t kMtimeOffset = 136;
static constexpr size_t kMtimeLength = 12;
static constexpr size_t kChecksumOffset = 148;
static constexpr size_t kChecksumLength = 8;
static constexpr size_t kTypeflagOffset = 156;
static constexpr size_t kMagicOffset = 257;
static constexpr size_t kMagicLength = 6;
static constexpr size_t kPrefixOffset = 345;
static constexpr size_t kPrefixLength = 155;
// Type flag values
static constexpr char kTypeRegular = '0';
static constexpr char kTypeRegularAlt = '\0'; // Old-style tar
static constexpr char kTypeHardLink = '1';
static constexpr char kTypeSymlink = '2';
static constexpr char kTypeDirectory = '5';
static constexpr char kTypePaxExtended = 'x'; // PAX extended header
static constexpr char kTypePaxGlobal = 'g'; // PAX global header
static constexpr char kTypeGnuLongName = 'L'; // GNU long filename
static constexpr char kTypeGnuLongLink = 'K'; // GNU long linkname
// Parse a tar header, returns nullopt if invalid or end-of-archive (zero
// block).
static std::optional<TarEntry> ParseHeader(
base::span<const uint8_t> header,
const std::string& override_name = std::string());
// Check if header is an end-of-archive marker (512 zero bytes).
static bool IsEndOfArchive(base::span<const uint8_t> header);
// Validate header checksum.
static bool ValidateChecksum(base::span<const uint8_t> header);
// Read octal number from header field (handles GNU extension for large
// files). Reference: SingleFileTarReader::ReadOctalNumber
static std::optional<uint64_t> ReadOctalNumber(
base::span<const uint8_t> field);
// Validate path is safe (no traversal, not absolute, no reserved names).
static bool IsPathSafe(const std::string& path_str);
// Parse PAX extended header content to extract path override.
// PAX format: "length key=value\n"
static std::optional<std::string> ParsePaxPath(
base::span<const uint8_t> content);
};
// XzDecompressor decompresses xz/LZMA2 streams.
// Reference: chrome/services/file_util/single_file_tar_xz_file_extractor.cc
class XzDecompressor {
public:
XzDecompressor();
~XzDecompressor();
XzDecompressor(const XzDecompressor&) = delete;
XzDecompressor& operator=(const XzDecompressor&) = delete;
// Decompress a chunk of xz data.
// Returns true on success, false on error.
// Appends decompressed data to |output|.
// Call repeatedly until IsFinished() returns true.
bool DecompressChunk(base::span<const uint8_t> input,
std::vector<uint8_t>* output,
bool input_finished = false);
// Returns true when the xz stream is completely decompressed.
bool IsFinished() const;
private:
// Opaque pointer to CXzUnpacker (avoid including lzma_sdk headers here)
void* state_ = nullptr;
void* alloc_ = nullptr;
};
} // namespace internal
} // namespace cef_installer
#endif // CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_ARCHIVE_H_
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,704 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cef/libcef_dll/bootstrap/installer/installer_bootstrap_helpers.h"
#include <algorithm>
#include <limits>
#include <utility>
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "cef/libcef_dll/bootstrap/installer/installer_config.h"
#include "cef/libcef_dll/bootstrap/installer/installer_e2e_config.h"
#include "cef/libcef_dll/bootstrap/installer/installer_launch_state.h"
#include "cef/libcef_dll/bootstrap/installer/installer_logger.h"
#include "cef/libcef_dll/bootstrap/installer/installer_paths.h"
namespace cef_installer {
std::optional<Command> ParseInstallerCommand(
const base::CommandLine& command_line) {
// Check for /cef-update or --cef-update
if (command_line.HasSwitch(kSwitchUpdate)) {
return Command::kUpdate;
}
// Check for /cef-uninstall or --cef-uninstall
if (command_line.HasSwitch(kSwitchUninstall)) {
return Command::kUninstall;
}
if (command_line.HasSwitch(kSwitchRetentionDryRun)) {
return Command::kRetentionDryRun;
}
if (command_line.HasSwitch(kSwitchRetentionApply)) {
return Command::kRetentionApply;
}
// No installer command present
return std::nullopt;
}
bool HasConflictingInstallerCommands(const base::CommandLine& command_line) {
int count = 0;
for (const char* name : {kSwitchUpdate, kSwitchUninstall,
kSwitchRetentionDryRun, kSwitchRetentionApply}) {
if (command_line.HasSwitch(name)) {
++count;
}
}
return count > 1;
}
bool HasMisappliedRetentionOptions(const base::CommandLine& command_line,
std::optional<Command> command) {
const bool retention_command = command == Command::kRetentionDryRun ||
command == Command::kRetentionApply;
return command_line.HasSwitch(kSwitchRetentionMaxAgeDays) &&
!retention_command;
}
bool ParseRetentionMaxAgeDays(const base::CommandLine& command_line,
int* max_age_days) {
if (!max_age_days) {
return false;
}
*max_age_days = kDefaultRetentionMaxAgeDays;
if (!command_line.HasSwitch(kSwitchRetentionMaxAgeDays)) {
return true;
}
int value = 0;
if (!base::StringToInt(
command_line.GetSwitchValueASCII(kSwitchRetentionMaxAgeDays),
&value) ||
!IsValidRetentionMaxAgeDays(value)) {
return false;
}
*max_age_days = value;
return true;
}
bool IsUninstallRelaunched(const base::CommandLine& command_line) {
return command_line.HasSwitch(kSwitchUninstallRelaunched);
}
bool ParseParentWindowSwitch(const base::CommandLine& command_line,
HWND* parent_window) {
if (!parent_window || !command_line.HasSwitch(kSwitchParentWindow)) {
return false;
}
const std::string& hwnd_str =
command_line.GetSwitchValueASCII(kSwitchParentWindow);
uint64_t hwnd_value = 0;
if (!base::StringToUint64(hwnd_str, &hwnd_value) || hwnd_value == 0 ||
hwnd_value > std::numeric_limits<uintptr_t>::max()) {
return false;
}
*parent_window = reinterpret_cast<HWND>(static_cast<uintptr_t>(hwnd_value));
return true;
}
void ParseExtendedConfigFromCommandLine(const base::CommandLine& command_line,
ExtendedConfig* extended_config) {
if (!extended_config) {
return;
}
// /cef-forcecheck - force version check even if recently checked
if (command_line.HasSwitch(kSwitchForceCheck)) {
extended_config->force_check = true;
VLOG(2) << "Extended config: force_check=true";
}
// /cef-background selects low-impact extraction and also suppresses UI.
// /cef-headless suppresses UI without changing extraction behavior.
extended_config->background_mode = command_line.HasSwitch(kSwitchBackground);
if (extended_config->background_mode ||
command_line.HasSwitch(kSwitchHeadless)) {
extended_config->show_progress_ui = false;
VLOG(2) << "Extended config: show_progress_ui=false";
}
// /cef-parent=<hwnd> - parent window handle for progress UI
if (command_line.HasSwitch(kSwitchParentWindow)) {
if (ParseParentWindowSwitch(command_line,
&extended_config->parent_window)) {
VLOG(2) << "Extended config: valid parent window supplied";
} else {
LOG(WARNING) << "Invalid parent window handle";
}
}
// /cef-download-path=<path> - read from local directory instead of CDN
if (command_line.HasSwitch(kSwitchDownloadPath)) {
std::string path = command_line.GetSwitchValueASCII(kSwitchDownloadPath);
if (!path.empty()) {
extended_config->local_download_path = path;
VLOG(2) << "Extended config: local_download_path=" << path;
} else {
LOG(WARNING) << "/cef-download-path requires a path value";
}
}
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
// /cef-install-path=<path> - override install directory
if (command_line.HasSwitch(kSwitchInstallPath)) {
std::string path = command_line.GetSwitchValueASCII(kSwitchInstallPath);
if (!path.empty()) {
extended_config->install_path = path;
VLOG(2) << "Extended config: install_path=" << path;
} else {
LOG(WARNING) << "/cef-install-path requires a path value";
}
}
#endif
// /cef-log-level=<level> - set minimum log level (info, warning, error)
if (command_line.HasSwitch(kSwitchLogLevel)) {
std::string level_str = command_line.GetSwitchValueASCII(kSwitchLogLevel);
auto level = LogLevelFromString(level_str);
if (level.has_value()) {
extended_config->log_level = level.value();
VLOG(2) << "Extended config: log_level=" << level_str;
} else {
LOG(WARNING) << "/cef-log-level invalid value: " << level_str
<< " (expected: info, warning, error)";
}
}
}
std::optional<Command> CommandFromString(const std::string& str) {
std::string lower = base::ToLowerASCII(str);
if (lower == "install") {
return Command::kInstall;
}
if (lower == "update") {
return Command::kUpdate;
}
if (lower == "uninstall") {
return Command::kUninstall;
}
if (lower == "query") {
return Command::kQuery;
}
if (lower == "prune") {
return Command::kPrune;
}
return std::nullopt;
}
ConfigLoadResult TryLoadInstallerConfig(HMODULE client_dll_module,
Config* config,
HMODULE bootstrap_module,
bool trusted_uninstall_relaunch) {
auto error_result = [](ConfigError error, const std::string& source,
const std::string& diagnostic) {
return ConfigLoadResult{
.status = ConfigLoadStatus::kError,
.error = error,
.diagnostic =
source + ": " +
(diagnostic.empty() ? ConfigErrorToString(error) : diagnostic)};
};
if (!config) {
return error_result(ConfigError::kMissingRequiredField, "Installer config",
"Config output is null");
}
if (trusted_uninstall_relaunch &&
internal::GetInstallerE2EConfig().child_config_failure) {
return error_result(ConfigError::kJsonParseError, "Installer config",
"Forced child config-load failure for E2E testing");
}
if (!bootstrap_module) {
bootstrap_module = ::GetModuleHandle(nullptr);
}
// When client DLL is present, use its config (or fall back to bootstrap).
if (client_dll_module) {
std::string client_diagnostic;
ConfigError client_error =
ReadConfigFromResource(client_dll_module, config,
{.allow_unchecked_cef_path = true,
.allow_bundled_cef_path = true,
.allow_install_path = true},
&client_diagnostic);
if (client_error != ConfigError::kSuccess &&
client_error != ConfigError::kResourceNotFound) {
return error_result(client_error, "Client DLL resource",
client_diagnostic);
}
// Read enable_explicit_modes from the bootstrap exe's own resource.
// This flag is only authoritative in the exe; the client DLL cannot set
// it, and it does not change application-config provenance.
Config bootstrap_config;
std::string bootstrap_diagnostic;
ConfigError bootstrap_error = ReadConfigFromResource(
bootstrap_module, &bootstrap_config,
{.allow_enable_explicit_modes = true}, &bootstrap_diagnostic);
if (bootstrap_error != ConfigError::kSuccess &&
bootstrap_error != ConfigError::kResourceNotFound) {
return error_result(bootstrap_error, "Bootstrap resource",
bootstrap_diagnostic);
}
if (client_error == ConfigError::kSuccess) {
VLOG(1) << "Installer config loaded from client DLL resource (appid="
<< config->appid << ", vmin=" << config->vmin << ")";
} else {
VLOG(2) << "Client DLL resource config not found";
if (bootstrap_error == ConfigError::kSuccess) {
*config = bootstrap_config;
VLOG(1) << "Installer config loaded from bootstrap resource (appid="
<< config->appid << ", vmin=" << config->vmin << ")";
return {.status = ConfigLoadStatus::kLoaded,
.source = ConfigLoadSource::kBootstrapResource};
}
VLOG(1) << "No installer config found";
return {.status = ConfigLoadStatus::kNotFound,
.error = ConfigError::kResourceNotFound,
.diagnostic = "No embedded installer config resource found"};
}
if (bootstrap_error == ConfigError::kSuccess) {
config->enable_explicit_modes = bootstrap_config.enable_explicit_modes;
}
return {.status = ConfigLoadStatus::kLoaded,
.source = ConfigLoadSource::kClientResource};
}
// Standalone mode (no client DLL).
std::string bootstrap_diagnostic;
ConfigError bootstrap_error = ReadConfigFromResource(
bootstrap_module, config, {.allow_enable_explicit_modes = true},
&bootstrap_diagnostic);
if (bootstrap_error == ConfigError::kSuccess) {
VLOG(1) << "Installer config loaded from bootstrap resource (appid="
<< config->appid << ", vmin=" << config->vmin << ")";
return {.status = ConfigLoadStatus::kLoaded,
.source = ConfigLoadSource::kBootstrapResource};
}
if (bootstrap_error != ConfigError::kResourceNotFound) {
return error_result(bootstrap_error, "Bootstrap resource",
bootstrap_diagnostic);
}
VLOG(1) << "No installer config found";
return {.status = ConfigLoadStatus::kNotFound,
.error = ConfigError::kResourceNotFound,
.diagnostic = "No embedded installer config resource found"};
}
bool ResolveClientInstallPath(HMODULE client_dll_module, Config* config) {
if (!config) {
return false;
}
if (config->install_path.empty()) {
return true;
}
if (!client_dll_module) {
return false;
}
wchar_t module_path[MAX_PATH];
const DWORD len =
::GetModuleFileNameW(client_dll_module, module_path, MAX_PATH);
if (len == 0 || len >= MAX_PATH) {
return false;
}
const base::FilePath resolved = ResolvePathRelativeTo(
config->install_path, base::FilePath(module_path).DirName());
if (resolved.empty()) {
return false;
}
config->install_path = resolved.AsUTF8Unsafe();
return true;
}
InstallerStartupState MakeInstallerStartupState(
const ConfigLoadResult& load_result,
const Config& config) {
InstallerStartupState state;
state.configured = load_result.status == ConfigLoadStatus::kLoaded;
if (state.configured) {
state.effective_vmin = config.vmin;
state.effective_vmax = config.vmax;
} else if (load_result.status == ConfigLoadStatus::kError) {
state.error_code = kExitCodeConfigError;
state.error_message = load_result.diagnostic;
}
return state;
}
void ApplyInstallerResultToStartupState(const Result& result,
const Config& config,
const ExtendedConfig& extended,
bool apply_launch_health,
InstallerStartupState* state) {
if (!state) {
return;
}
state->configured = true;
state->effective_vmin = config.vmin;
state->effective_vmax = config.vmax;
state->config = config;
state->extended = extended;
state->launch_health = config.launch_health;
state->launch_state_path.clear();
state->launch_consecutive_failures = 0;
state->launch_version.clear();
state->launch_platform.clear();
state->launch_cleanup_paths.clear();
state->liveness_path.clear();
state->appid.clear();
state->appid_hash.clear();
state->version_lease.reset();
if (result.success) {
state->libcef_path = result.libcef_path;
state->is_bundled = result.is_bundled;
state->version_full = result.version_full;
state->error_code = kExitCodeSuccess;
state->error_message.clear();
state->version_lease = result.version_lease;
state->liveness_path = result.liveness_path;
if (apply_launch_health && !result.libcef_path.empty()) {
state->launch_state_path = result.launch_state_path;
state->launch_consecutive_failures = result.launch_consecutive_failures;
state->launch_version = result.launch_version;
state->launch_platform = result.launch_platform;
state->launch_cleanup_paths = result.launch_cleanup_paths;
state->appid = config.appid;
state->appid_hash = GetAppidHash(config.appid);
}
} else {
state->libcef_path.clear();
state->is_bundled = false;
state->version_full.clear();
state->error_code = result.error_code;
state->error_message = result.error_message;
}
}
void PopulateInstallerVersionInfo(const InstallerStartupState& state,
cef_version_info_t* info) {
if (!info) {
return;
}
#if CEF_API_ADDED(CEF_NEXT)
if (info->size >= CEF_VERSION_INFO_SIZE_WITH_INSTALLER_ERROR) {
info->libcef_path =
state.libcef_path.empty() ? nullptr : state.libcef_path.value().c_str();
info->libcef_is_bundled = state.is_bundled;
info->libcef_version_full =
state.version_full.empty() ? nullptr : state.version_full.c_str();
info->installer_error_code = state.error_code;
info->installer_error_message =
state.error_message.empty() ? nullptr : state.error_message.c_str();
}
#endif
}
InstallerCrashAnnotationValues BuildInstallerCrashAnnotationValues(
const InstallerStartupState& state) {
InstallerCrashAnnotationValues values;
if (state.error_code == kExitCodeSuccess) {
return values;
}
values.has_failure = true;
values.error = base::NumberToString(state.error_code);
values.message = state.error_message;
values.vmin = state.effective_vmin;
values.vmax =
state.effective_vmax.empty() ? "unbounded" : state.effective_vmax;
if (state.error_code == kExitCodePolicyDenied) {
values.policy_denied = "1";
}
if (state.error_code == kExitCodeCancelled) {
values.cancelled = "1";
}
return values;
}
namespace internal {
namespace {
bool WriteAllToHandleImpl(HANDLE handle,
std::string_view data,
DWORD max_chunk_size,
HandleWriteCallback write) {
if (!handle || handle == INVALID_HANDLE_VALUE) {
::SetLastError(ERROR_INVALID_HANDLE);
return false;
}
if (max_chunk_size == 0 || write.is_null()) {
::SetLastError(ERROR_INVALID_PARAMETER);
return false;
}
while (!data.empty()) {
const DWORD chunk_size = static_cast<DWORD>(
std::min(data.size(), static_cast<size_t>(max_chunk_size)));
DWORD written = 0;
if (!write.Run(handle, data.data(), chunk_size, &written)) {
return false;
}
if (written == 0 || written > chunk_size) {
::SetLastError(ERROR_WRITE_FAULT);
return false;
}
data.remove_prefix(written);
}
return true;
}
} // namespace
UninstallInvocationState ClassifyUninstallInvocation(
std::optional<Command> command,
bool has_relaunch_marker,
bool has_relaunch_state,
bool trusted_state_valid) {
if (!has_relaunch_marker && !has_relaunch_state) {
return command == Command::kUninstall
? UninstallInvocationState::kOriginal
: UninstallInvocationState::kNotUninstall;
}
if (command != Command::kUninstall || !has_relaunch_marker ||
!has_relaunch_state || !trusted_state_valid) {
return UninstallInvocationState::kInvalid;
}
return UninstallInvocationState::kRelaunched;
}
} // namespace internal
std::optional<UninstallInvocationContext> ResolveUninstallInvocationContext(
const base::CommandLine& command_line,
const base::FilePath& exe_path,
std::optional<Command> command) {
const bool has_relaunch_marker = IsUninstallRelaunched(command_line);
const bool has_relaunch_state = command_line.HasSwitch(kSwitchUninstallState);
bool trusted_state_valid = false;
UninstallRelaunchState relaunch_state;
if (command == Command::kUninstall && has_relaunch_marker &&
has_relaunch_state) {
HWND command_parent_window = nullptr;
ParseParentWindowSwitch(command_line, &command_parent_window);
trusted_state_valid = internal::ReadUninstallRelaunchState(
exe_path, command_line.GetSwitchValueASCII(kSwitchUninstallState),
command_parent_window, &relaunch_state);
if (trusted_state_valid) {
internal::WaitAtInstallerE2EChildStateBarrier();
}
}
const UninstallInvocationState invocation =
internal::ClassifyUninstallInvocation(command, has_relaunch_marker,
has_relaunch_state,
trusted_state_valid);
if (invocation == UninstallInvocationState::kInvalid) {
return std::nullopt;
}
return UninstallInvocationContext{invocation, std::move(relaunch_state)};
}
namespace internal {
UninstallExecutionDecision DecideUninstallExecution(
UninstallInvocationState invocation,
bool has_writable_target,
bool background_mode,
PathContainment containment) {
if (invocation == UninstallInvocationState::kInvalid ||
invocation == UninstallInvocationState::kNotUninstall) {
return {.execution = UninstallExecution::kReject,
.error_code = kExitCodeConfigError};
}
if (invocation == UninstallInvocationState::kRelaunched ||
!has_writable_target) {
return {.execution = UninstallExecution::kInProcess,
.error_code = kExitCodeSuccess};
}
if (background_mode || containment == PathContainment::kContained) {
return {.execution = UninstallExecution::kRelaunch,
.error_code = kExitCodeSuccess};
}
if (containment == PathContainment::kOutside) {
return {.execution = UninstallExecution::kInProcess,
.error_code = kExitCodeSuccess};
}
return {.execution = UninstallExecution::kReject,
.error_code = kExitCodeInstallError};
}
} // namespace internal
int UninstallRelaunchExitCode(UninstallRelaunchStatus status) {
return status == UninstallRelaunchStatus::kStarted ? kExitCodeRelaunched
: kExitCodeInstallError;
}
UninstallPreflightResult PrepareUninstall(const Config& config,
const ExtendedConfig& extended,
const base::FilePath& executable_path,
UninstallInvocationState invocation) {
UninstallPreflightResult result;
if (invocation != UninstallInvocationState::kOriginal &&
invocation != UninstallInvocationState::kRelaunched) {
result.decision = internal::DecideUninstallExecution(
invocation, false, extended.background_mode,
internal::PathContainment::kIndeterminate);
return result;
}
if (invocation == UninstallInvocationState::kOriginal) {
const internal::InstallerE2EConfig& e2e = internal::GetInstallerE2EConfig();
if (e2e.directory_override ==
internal::InstallerE2EDirectoryOverride::kInvalid) {
result.decision = {UninstallExecution::kReject, kExitCodeConfigError};
return result;
}
if (e2e.directory_override ==
internal::InstallerE2EDirectoryOverride::kAdminMutationDenied) {
const base::FilePath admin = e2e.directory_root.Append(L"Admin");
const base::FilePath per_user = e2e.directory_root.Append(L"PerUser");
if (!base::DirectoryExists(admin) || !base::DirectoryExists(per_user)) {
result.decision = {UninstallExecution::kReject, kExitCodeConfigError};
return result;
}
internal::OverrideInstallDirectoryCandidatesForTesting({
{admin, DirectoryRole::kHklmDefault, true, true},
{per_user, DirectoryRole::kPerUserDefault, true, true},
});
internal::OverrideProcessElevationForTesting(true);
internal::OverrideAdminMutationAllowedForTesting(false);
}
}
PreparedUninstall prepared;
prepared.config_binding = ConfigToJson(config);
prepared.install_path = extended.install_path;
prepared.resolution_context.mutation_capable = true;
prepared.resolution_context.is_elevated = IsCurrentProcessElevated();
prepared.resolution_context.allow_admin_mutation =
IsAdminMutationAllowed(false, config.enable_explicit_modes);
prepared.policy_result = LoadEnterprisePolicy();
if (prepared.policy_result.valid()) {
prepared.resolution_context.allow_shared_user_store =
prepared.policy_result.policy.allow_shared_user_store;
prepared.directories = ResolveInstallDirectories(
extended.install_path, prepared.resolution_context);
}
const bool has_writable_target =
prepared.directories &&
prepared.directories->write_error == PathError::kSuccess &&
!prepared.directories->writable_dir.empty();
internal::PathContainment containment =
internal::PathContainment::kIndeterminate;
if (invocation == UninstallInvocationState::kOriginal &&
has_writable_target && !extended.background_mode) {
containment = internal::GetPhysicalPathContainment(
prepared.directories->writable_dir, executable_path);
}
result.decision = internal::DecideUninstallExecution(
invocation, has_writable_target, extended.background_mode, containment);
prepared.execution = result.decision.execution;
result.prepared = std::move(prepared);
return result;
}
std::string FormatRetentionOutput(const Result& result, bool json_output) {
if (json_output) {
return result.ToJson() + "\r\n";
}
std::string output =
"retention outcome=" + std::string(OutcomeToString(result.outcome)) +
" max_age_days=" + base::NumberToString(result.retention_max_age_days) +
" registrations_committed=" +
(result.registrations_committed ? "true" : "false") +
" versions_pruned=" + (result.versions_pruned ? "true" : "false") +
" retry_required=" + (result.retry_required ? "true" : "false") + "\r\n";
if (result.retention_plan) {
if (!result.retention_plan->blocker.empty()) {
output += "store blocker=" + result.retention_plan->blocker + "\r\n";
}
for (const auto& registration : result.retention_plan->registrations) {
output +=
"registration appid=" + registration.entry.uuid +
" platform=" + registration.entry.platform +
" vmin=" + registration.entry.vmin +
" vmax=" + registration.entry.vmax +
" abi_hash=" + registration.entry.abi_hash + " evidence_kind=" +
RetentionEvidenceKindToString(registration.evidence.kind) +
" evidence_time=" +
base::NumberToString(registration.evidence.timestamp) + " age_days=" +
(registration.age_days ? base::NumberToString(*registration.age_days)
: "unknown") +
" decision=" +
RetentionRegistrationDecisionToString(registration.decision) +
" reason=" + RetentionReasonToString(registration.reason);
if (!registration.evidence.diagnostic.empty()) {
output += " diagnostic=" + registration.evidence.diagnostic;
}
output += "\r\n";
}
for (const auto& version : result.retention_plan->versions) {
output +=
"version value=" + version.version.ToString() +
" platform=" + version.platform +
" required_before=" + (version.required_before ? "true" : "false") +
" required_after=" + (version.required_after ? "true" : "false") +
" expected_removal=" + (version.expected_removal ? "true" : "false") +
" cleanup_deferred=" + (version.cleanup_deferred ? "true" : "false") +
" decision=" + RetentionVersionDecisionToString(version.decision) +
" reason=" + RetentionReasonToString(version.reason) + "\r\n";
}
}
if (!result.error_message.empty()) {
output += "error=" + result.error_message + "\r\n";
}
std::vector<std::string> warnings = result.warnings;
std::ranges::sort(warnings);
for (const auto& warning : warnings) {
output += "warning=" + warning + "\r\n";
}
return output;
}
bool WriteAllToHandle(HANDLE handle, std::string_view data) {
return internal::WriteAllToHandleImpl(
handle, data, std::numeric_limits<DWORD>::max(),
base::BindRepeating([](HANDLE destination, const char* bytes, DWORD size,
DWORD* written) {
return !!::WriteFile(destination, bytes, size, written, nullptr);
}));
}
namespace internal {
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
bool WriteAllToHandleForTesting(HANDLE handle,
std::string_view data,
DWORD max_chunk_size,
HandleWriteCallback write) {
return WriteAllToHandleImpl(handle, data, max_chunk_size, std::move(write));
}
#endif
} // namespace internal
} // namespace cef_installer
@@ -0,0 +1,300 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_BOOTSTRAP_HELPERS_H_
#define CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_BOOTSTRAP_HELPERS_H_
#include <windows.h>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "cef/include/cef_version_info.h"
#include "cef/libcef_dll/bootstrap/installer/installer_config.h"
#include "cef/libcef_dll/bootstrap/installer/installer_constants.h"
#include "cef/libcef_dll/bootstrap/installer/installer_controller.h"
#include "cef/libcef_dll/bootstrap/installer/installer_relaunch.h"
namespace cef_installer {
// Command-line switch names for installer modes (used with / prefix on Windows)
// These are parsed from the command line when running in standalone mode.
constexpr const char kSwitchUpdate[] = "cef-update";
constexpr const char kSwitchUninstall[] = "cef-uninstall";
constexpr const char kSwitchRetentionDryRun[] = "cef-retention-dry-run";
constexpr const char kSwitchRetentionApply[] = "cef-retention-apply";
constexpr const char kSwitchRetentionMaxAgeDays[] = "cef-max-age-days";
constexpr const char kSwitchUninstallRelaunched[] = "cef-uninstall-relaunched";
// Internal marker indicating that a self-relocated uninstall must load the
// trusted relaunch state written beside the temporary executable.
constexpr const char kSwitchUninstallState[] = "cef-uninstall-state";
constexpr const char kSwitchForceCheck[] = "cef-forcecheck";
constexpr const char kSwitchBackground[] = "cef-background";
constexpr const char kSwitchHeadless[] = "cef-headless";
constexpr const char kSwitchParentWindow[] = "cef-parent";
// /cef-download-path=<path> - Read manifests and archives from a local
// directory instead of downloading from CDN. Works as a modifier for any
// command (/cef-update, /cef-uninstall, auto-install). All post-download
// validation (hash, signature, size limits) still applies.
constexpr const char kSwitchDownloadPath[] = "cef-download-path";
// /cef-log-level=<level> - Set minimum log level for cef_installer.log.
// Accepted values: info, warning, error (case-insensitive). Default: warning.
constexpr const char kSwitchLogLevel[] = "cef-log-level";
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
// /cef-install-path=<path> - Override the CEF installation directory. In
// standalone mode, bypasses the normal directory resolution (registry ->
// ProgramFiles -> LocalAppData). When a client DLL is present, install_path
// is passed programmatically via the RunInstaller JSON instead. Only
// available in non-official builds for testing.
constexpr const char kSwitchInstallPath[] = "cef-install-path";
#endif
// ============================================================================
// Command-line Parsing
// ============================================================================
// Parse installer command from command-line arguments.
// Returns the Command if /cef-update or /cef-uninstall is present, nullopt
// otherwise. On Windows, switches can use either -- or / prefix (e.g.,
// /cef-update or --cef-update)
std::optional<Command> ParseInstallerCommand(
const base::CommandLine& command_line);
// Returns true when more than one explicit installer operation was supplied.
bool HasConflictingInstallerCommands(const base::CommandLine& command_line);
// Returns true if retention-only options are present without a retention
// operation.
bool HasMisappliedRetentionOptions(const base::CommandLine& command_line,
std::optional<Command> command);
// Parse /cef-max-age-days for retention commands. Returns false for a missing
// value, non-integer, or value outside 90..3650.
bool ParseRetentionMaxAgeDays(const base::CommandLine& command_line,
int* max_age_days);
// Check if the internal --cef-uninstall-relaunched transport marker is
// present. The marker alone is never trusted; bootstrap also requires the
// nonce-bound state file written beside the temporary executable.
bool IsUninstallRelaunched(const base::CommandLine& command_line);
// Parse extended configuration flags from command-line.
// Modifies the provided ExtendedConfig based on command-line switches.
// Recognized switches:
// /cef-forcecheck - set force_check = true
// /cef-background - set show_progress_ui = false (also sets low priority)
// /cef-headless - set show_progress_ui = false
// /cef-parent=<hwnd> - set parent_window to specified HWND value
// /cef-download-path=<path> - set local_download_path
// /cef-log-level=<level> - set log_level (info, warning, error)
void ParseExtendedConfigFromCommandLine(const base::CommandLine& command_line,
ExtendedConfig* extended_config);
// Parses /cef-parent as an unsigned pointer-width decimal value. This does not
// require the HWND to remain valid; callers decide whether current IsWindow
// validity is required for their phase.
bool ParseParentWindowSwitch(const base::CommandLine& command_line,
HWND* parent_window);
// ============================================================================
// Utility Functions
// ============================================================================
// Parse Command from string (case-insensitive).
// Accepts: "install", "update", "uninstall", "query"
// Returns nullopt if string doesn't match any valid command.
std::optional<Command> CommandFromString(const std::string& str);
// ============================================================================
// Config Loading
// ============================================================================
// Try to load installer config using the resource priority chain:
// 1. Client DLL resource (if client_dll_module != nullptr)
// 2. Bootstrap resource (fallback when client DLL has no config, or
// as the standalone source)
//
// enable_explicit_modes is always read from the bootstrap exe's resource,
// regardless of which source provides the application config. The client DLL
// cannot set this flag, and application-config provenance never describes it.
//
// |bootstrap_module| defaults to GetModuleHandle(nullptr) when nullptr.
// Testable via explicit module handles for the bootstrap_module parameter.
enum class ConfigLoadStatus {
kLoaded,
kNotFound,
kError,
};
// Internal provenance for a successfully loaded application config. kNone is
// required for not-found and every error result.
enum class ConfigLoadSource {
kNone,
kClientResource,
kBootstrapResource,
};
struct ConfigLoadResult {
ConfigLoadStatus status = ConfigLoadStatus::kNotFound;
ConfigError error = ConfigError::kSuccess;
ConfigLoadSource source = ConfigLoadSource::kNone;
std::string diagnostic;
};
ConfigLoadResult TryLoadInstallerConfig(
HMODULE client_dll_module,
Config* config,
HMODULE bootstrap_module = nullptr,
bool trusted_uninstall_relaunch = false);
// Resolves a non-empty client-resource install_path relative to the module
// directory. On failure the value remains non-empty so exclusive resolution
// cannot silently fall back to a default store.
bool ResolveClientInstallPath(HMODULE client_dll_module, Config* config);
// Owned installer state that remains alive through the client entry point.
struct InstallerStartupState {
bool configured = false;
base::FilePath libcef_path;
bool is_bundled = false;
std::string version_full;
int error_code = kExitCodeSuccess;
std::string error_message;
std::string effective_vmin;
std::string effective_vmax;
// Launch-health handoff.
LaunchHealthMode launch_health = LaunchHealthMode::kOff;
base::FilePath launch_state_path;
int launch_consecutive_failures = 0;
std::string launch_version;
std::string launch_platform;
std::vector<base::FilePath> launch_cleanup_paths;
base::FilePath liveness_path;
std::string appid;
std::wstring appid_hash;
Config config;
ExtendedConfig extended;
std::shared_ptr<VersionLease> version_lease;
};
InstallerStartupState MakeInstallerStartupState(
const ConfigLoadResult& load_result,
const Config& config);
void ApplyInstallerResultToStartupState(const Result& result,
const Config& config,
const ExtendedConfig& extended,
bool apply_launch_health,
InstallerStartupState* state);
void PopulateInstallerVersionInfo(const InstallerStartupState& state,
cef_version_info_t* info);
struct InstallerCrashAnnotationValues {
bool has_failure = false;
std::string error;
std::string message;
std::string vmin;
std::string vmax;
std::string policy_denied;
std::string cancelled;
};
InstallerCrashAnnotationValues BuildInstallerCrashAnnotationValues(
const InstallerStartupState& state);
// Trusted relaunch classification. kInvalid covers a marker/state mismatch,
// invalid trusted state, or relaunch transport state on a non-uninstall
// command. kNotUninstall is the ordinary no-transport case.
enum class UninstallInvocationState {
kNotUninstall,
kOriginal,
kRelaunched,
kInvalid,
};
struct UninstallInvocationContext {
UninstallInvocationState invocation = UninstallInvocationState::kInvalid;
UninstallRelaunchState relaunch_state;
};
struct UninstallExecutionDecision {
UninstallExecution execution = UninstallExecution::kReject;
int error_code = kExitCodeConfigError;
};
// Validates the relaunch marker/state command shape and loads the minimal
// nonce-bound state needed before installer config is available. Returns null
// for malformed transport or untrusted state.
std::optional<UninstallInvocationContext> ResolveUninstallInvocationContext(
const base::CommandLine& command_line,
const base::FilePath& exe_path,
std::optional<Command> command);
namespace internal {
// Classifies the relaunch marker/state pair after any trusted state read.
UninstallInvocationState ClassifyUninstallInvocation(
std::optional<Command> command,
bool has_relaunch_marker,
bool has_relaunch_state,
bool trusted_state_valid);
// Pure execution matrix used by bootstrap preflight and table-driven tests.
UninstallExecutionDecision DecideUninstallExecution(
UninstallInvocationState invocation,
bool has_writable_target,
bool background_mode,
PathContainment containment);
} // namespace internal
// A successful child launch is pending (109); any relaunch preparation or
// process-creation failure is an installer setup failure (105).
int UninstallRelaunchExitCode(UninstallRelaunchStatus status);
struct UninstallPreflightResult {
UninstallExecutionDecision decision;
std::optional<PreparedUninstall> prepared;
};
// Loads immutable policy, resolves the explicit mutation-permitted uninstall
// target once, and chooses direct execution or asynchronous relaunch.
// For an original, non-background uninstall with a writable target,
// indeterminate physical containment returns 105 before mutation or relaunch.
// A valid relaunched child also prepares a fresh snapshot but is always
// in-process.
UninstallPreflightResult PrepareUninstall(const Config& config,
const ExtendedConfig& extended,
const base::FilePath& executable_path,
UninstallInvocationState invocation);
// Format explicit retention output for the console. Headless mode returns one
// JSON object followed by CRLF; normal mode returns deterministic text.
std::string FormatRetentionOutput(const Result& result, bool json_output);
// Write all bytes to a Win32 handle, retrying partial writes.
bool WriteAllToHandle(HANDLE handle, std::string_view data);
namespace internal {
using HandleWriteCallback =
base::RepeatingCallback<bool(HANDLE, const char*, DWORD, DWORD*)>;
// Injectable write implementation and chunk bound for deterministic tests.
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
bool WriteAllToHandleForTesting(HANDLE handle,
std::string_view data,
DWORD max_chunk_size,
HandleWriteCallback write);
#endif
} // namespace internal
} // namespace cef_installer
#endif // CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_BOOTSTRAP_HELPERS_H_
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,422 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cef/libcef_dll/bootstrap/installer/installer_cdn_manifest.h"
#include <algorithm>
#include <set>
#include "base/json/json_reader.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "build/build_config.h"
#include "cef/libcef_dll/bootstrap/installer/installer_paths.h"
#include "cef/libcef_dll/bootstrap/installer/installer_validation.h"
#include "cef/libcef_dll/bootstrap/installer/installer_version.h"
namespace cef_installer {
namespace {
// Validate that a filename is safe for URL construction.
// Prevents path traversal and URL redirection attacks.
bool IsValidArchiveFilename(const std::string& filename) {
if (filename.empty() || filename.size() > kMaxFilenameLength) {
return false;
}
// Must not contain path separators or URL indicators
if (filename.find('/') != std::string::npos ||
filename.find('\\') != std::string::npos ||
filename.find(':') != std::string::npos ||
filename.find("..") != std::string::npos) {
return false;
}
// Must not start with a dot (hidden files)
if (filename[0] == '.') {
return false;
}
// Must look like a valid archive filename.
// Only .tar.xz is supported by the extraction code (ExtractTarXz).
return base::EndsWith(filename, ".tar.xz",
base::CompareCase::INSENSITIVE_ASCII);
}
const char* CdnBuildExclusionReasonToString(CdnBuildExclusionReason reason) {
switch (reason) {
case CdnBuildExclusionReason::kAlreadyInstalled:
return "already installed";
case CdnBuildExclusionReason::kRevoked:
return "revoked";
case CdnBuildExclusionReason::kLaunchDisqualified:
return "disqualified by launch health";
case CdnBuildExclusionReason::kPriorDownloadFailure:
return "prior download failure";
}
return "unknown exclusion";
}
// Helper to parse a build entry from a JSON dict
// Includes field length validation to prevent memory issues from malformed
// data.
ManifestError ParseBuildEntry(const base::DictValue& dict,
CdnBuildEntry* entry) {
const std::string* version = dict.FindString("version");
if (!version || version->empty()) {
return ManifestError::kMissingRequiredField;
}
if (version->size() > kMaxVersionLength) {
return ManifestError::kInvalidFieldValue;
}
entry->version = Version::Parse(*version);
if (!entry->version.IsValid()) {
return ManifestError::kInvalidFieldValue;
}
const std::string* file = dict.FindString("file");
if (!file || file->empty()) {
return ManifestError::kMissingRequiredField;
}
if (!IsValidArchiveFilename(*file)) {
return ManifestError::kInvalidFieldValue;
}
entry->file = *file;
const std::string* sha1 = dict.FindString("sha1");
if (!sha1 || sha1->empty()) {
return ManifestError::kMissingRequiredField;
}
if (sha1->size() > kMaxSha1Length) {
return ManifestError::kInvalidFieldValue;
}
entry->sha1 = *sha1;
// last_modified is optional but expected
const std::string* last_modified = dict.FindString("last_modified");
if (last_modified && last_modified->size() > kMaxTimestampLength) {
return ManifestError::kInvalidFieldValue;
}
entry->last_modified = last_modified ? *last_modified : "";
// abi_hash is optional (may not be present on all platforms).
// Must be hex-only to prevent URL metacharacter injection in BuildAbiHashUrl.
const std::string* abi_hash = dict.FindString("abi_hash");
if (abi_hash && !abi_hash->empty()) {
if (!IsValidAbiHash(*abi_hash)) {
return ManifestError::kInvalidFieldValue;
}
entry->abi_hash = *abi_hash;
} else {
entry->abi_hash.clear();
}
return ManifestError::kSuccess;
}
// Ensure URL ends with a slash
std::string NormalizeBaseUrl(const std::string& base_url) {
if (base_url.empty() || base_url.back() == '/') {
return base_url;
}
return base_url + "/";
}
} // namespace
ManifestError ParseStableMilestone(const std::string& content, int* milestone) {
if (!milestone) {
return ManifestError::kMissingRequiredField;
}
// Trim whitespace
std::string trimmed = content;
base::TrimWhitespaceASCII(trimmed, base::TRIM_ALL, &trimmed);
if (trimmed.empty()) {
return ManifestError::kMissingRequiredField;
}
int value = 0;
if (!base::StringToInt(trimmed, &value)) {
return ManifestError::kInvalidFieldValue;
}
if (value <= 0) {
return ManifestError::kInvalidFieldValue;
}
*milestone = value;
return ManifestError::kSuccess;
}
ManifestError ParseMilestoneManifest(
const std::string& json,
std::map<std::string, CdnBuildEntry>* entries) {
if (!entries) {
return ManifestError::kMissingRequiredField;
}
entries->clear();
std::optional<base::DictValue> parsed =
base::JSONReader::ReadDict(json, base::JSON_PARSE_RFC);
if (!parsed) {
return ManifestError::kJsonParseError;
}
// Each key is a platform name, value is a build entry
for (const auto [platform, value] : *parsed) {
const base::DictValue* entry_dict = value.GetIfDict();
if (!entry_dict) {
return ManifestError::kJsonParseError;
}
CdnBuildEntry entry;
ManifestError entry_err = ParseBuildEntry(*entry_dict, &entry);
if (entry_err != ManifestError::kSuccess) {
return entry_err;
}
(*entries)[platform] = std::move(entry);
}
return ManifestError::kSuccess;
}
ManifestError ParsePlatformManifest(const std::string& json,
std::vector<CdnBuildEntry>* entries) {
if (!entries) {
return ManifestError::kMissingRequiredField;
}
entries->clear();
std::optional<base::ListValue> parsed =
base::JSONReader::ReadList(json, base::JSON_PARSE_RFC);
if (!parsed) {
return ManifestError::kJsonParseError;
}
for (const base::Value& item : *parsed) {
const base::DictValue* entry_dict = item.GetIfDict();
if (!entry_dict) {
return ManifestError::kJsonParseError;
}
CdnBuildEntry entry;
ManifestError entry_err = ParseBuildEntry(*entry_dict, &entry);
if (entry_err != ManifestError::kSuccess) {
return entry_err;
}
entries->push_back(std::move(entry));
}
return ManifestError::kSuccess;
}
namespace {
// Returns the channel suffix for URLs (empty for stable, "_beta" for beta)
std::string GetChannelSuffix(const std::string& channel) {
if (!channel.empty()) {
return "_" + channel;
}
return ""; // empty = stable, no suffix
}
// Returns the channel name for txt files ("stable" if empty, otherwise channel)
std::string GetChannelName(const std::string& channel) {
return channel.empty() ? std::string(kChannelStable) : channel;
}
} // namespace
std::string BuildChannelUrl(const std::string& base_url,
const std::string& channel) {
// stable.txt or beta.txt
return NormalizeBaseUrl(base_url) + GetChannelName(channel) + ".txt";
}
std::string BuildStableUrl(const std::string& base_url) {
return BuildChannelUrl(base_url, "");
}
std::string BuildMilestoneUrl(const std::string& base_url,
int milestone,
const std::string& channel) {
// stable: 137.json, beta: 137_beta.json
return NormalizeBaseUrl(base_url) + base::NumberToString(milestone) +
GetChannelSuffix(channel) + ".json";
}
std::string BuildPlatformUrl(const std::string& base_url,
int milestone,
const std::string& platform,
const std::string& channel) {
// stable: 137_windows64.json, beta: 137_windows64_beta.json
return NormalizeBaseUrl(base_url) + base::NumberToString(milestone) + "_" +
platform + GetChannelSuffix(channel) + ".json";
}
std::string BuildAbiHashUrl(const std::string& base_url,
const std::string& abi_hash,
const std::string& platform,
const std::string& channel) {
// stable: abc123_windows64.json, beta: abc123_windows64_beta.json
return NormalizeBaseUrl(base_url) + abi_hash + "_" + platform +
GetChannelSuffix(channel) + ".json";
}
std::string BuildArchiveUrl(const std::string& base_url,
const std::string& filename) {
return NormalizeBaseUrl(base_url) + filename;
}
std::string BuildHashFileUrl(const std::string& base_url,
const std::string& filename) {
return NormalizeBaseUrl(base_url) + filename + ".sha256";
}
std::string BuildRevocationListUrl(const std::string& base_url) {
return NormalizeBaseUrl(base_url) + std::string(kRevocationListPath);
}
std::optional<CdnBuildEntry> FindBestBuildEntry(
const std::vector<CdnBuildEntry>& entries,
const std::string& vmin,
const std::string& vmax,
const std::string& abi_hash,
const std::set<Version>& skip_versions) {
std::optional<CdnBuildEntry> best;
Version best_version;
for (const auto& entry : entries) {
// Skip versions already installed locally.
if (skip_versions.count(entry.version) > 0) {
continue;
}
// Filter by abi_hash if specified
if (!abi_hash.empty() && entry.abi_hash != abi_hash) {
continue;
}
// Filter by version range
if (!entry.version.IsInRange(vmin, vmax)) {
continue;
}
// Select the newest matching version
if (!best || entry.version > best_version) {
best = entry;
best_version = entry.version;
}
}
return best;
}
std::string BuildNoMatchingCdnVersionMessage(
const std::vector<CdnBuildEntry>& entries,
const std::string& platform,
const std::string& vmin,
const std::string& vmax,
const std::string& abi_hash,
const CdnBuildExclusionReasons& exclusion_reasons) {
std::string message =
"No CDN version matches requirements (platform = " + platform +
", version >= " + vmin;
if (!vmax.empty()) {
message += ", version <= " + vmax;
}
if (!abi_hash.empty()) {
message += ", ABI hash = " + abi_hash;
}
message += ")";
if (entries.empty()) {
return message + "; no validated CDN manifest candidates were available";
}
constexpr size_t kMaxDiagnosticCandidates = 5;
const size_t candidate_count =
std::min(entries.size(), kMaxDiagnosticCandidates);
const Version min_version = Version::Parse(vmin);
const Version max_version = Version::Parse(vmax);
message += "; candidate failures: ";
for (size_t i = 0; i < candidate_count; ++i) {
if (i > 0) {
message += "; ";
}
const CdnBuildEntry& entry = entries[i];
message += entry.version.ToString() + " [";
const auto exclusion = exclusion_reasons.find(entry.version);
if (exclusion != exclusion_reasons.end()) {
message += "excluded: ";
size_t reason_index = 0;
for (CdnBuildExclusionReason reason : exclusion->second) {
if (reason_index++ > 0) {
message += ", ";
}
message += CdnBuildExclusionReasonToString(reason);
}
if (exclusion->second.empty()) {
message += "unknown reason";
}
} else {
bool has_failure = false;
auto AppendFailure = [&](const std::string& failure) {
if (has_failure) {
message += ", ";
}
message += failure;
has_failure = true;
};
if (!abi_hash.empty() && entry.abi_hash != abi_hash) {
AppendFailure(
"ABI hash mismatch (got " +
(entry.abi_hash.empty() ? std::string("<empty>") : entry.abi_hash) +
")");
}
if (!entry.version.IsInRange(vmin, vmax)) {
if (min_version.IsValid() && entry.version < min_version) {
AppendFailure("version below vmin");
} else if (max_version.IsValid() && entry.version > max_version) {
AppendFailure("version above vmax");
} else {
AppendFailure("version outside required range");
}
}
if (!has_failure) {
message += "matched compatibility requirements but was excluded";
}
}
message += "]";
}
if (entries.size() > candidate_count) {
message += "; ... " +
base::NumberToString(entries.size() - candidate_count) +
" more candidate(s)";
}
return message;
}
const char* ManifestErrorToString(ManifestError error) {
switch (error) {
case ManifestError::kSuccess:
return "Success";
case ManifestError::kJsonParseError:
return "JSON parse error";
case ManifestError::kMissingRequiredField:
return "Missing required field";
case ManifestError::kInvalidFieldValue:
return "Invalid field value";
}
return "Unknown error";
}
} // namespace cef_installer
@@ -0,0 +1,127 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_CDN_MANIFEST_H_
#define CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_CDN_MANIFEST_H_
#include <map>
#include <optional>
#include <set>
#include <string>
#include <vector>
#include "cef/libcef_dll/bootstrap/installer/installer_constants.h"
#include "cef/libcef_dll/bootstrap/installer/installer_version.h"
namespace cef_installer {
// Represents a single CEF build entry from CDN manifest.
struct CdnBuildEntry {
Version version; // Short version (e.g., "137.3.5")
std::string file; // Signed archive filename
std::string sha1; // SHA1 hash of the signed archive
std::string last_modified; // ISO 8601 timestamp
std::string abi_hash; // Sandbox compatibility hash (may be empty)
};
enum class CdnBuildExclusionReason {
kAlreadyInstalled,
kRevoked,
kLaunchDisqualified,
kPriorDownloadFailure,
};
using CdnBuildExclusionReasons =
std::map<Version, std::set<CdnBuildExclusionReason>>;
// Error codes for CDN manifest operations.
enum class ManifestError {
kSuccess,
kJsonParseError, // Malformed JSON
kMissingRequiredField, // Required field not present
kInvalidFieldValue, // Field present but invalid format
};
// Parse stable.txt content (just a milestone number).
// Content example: "137"
ManifestError ParseStableMilestone(const std::string& content, int* milestone);
// Parse {milestone}.json - newest build per platform.
// Returns map of platform -> build entry.
// Example platforms: "windows64", "windows32", "windowsarm64", "linux64"
ManifestError ParseMilestoneManifest(
const std::string& json,
std::map<std::string, CdnBuildEntry>* entries);
// Parse {milestone}_{platform}.json or {abi_hash}_{platform}.json.
// Returns list of builds sorted newest-first.
ManifestError ParsePlatformManifest(const std::string& json,
std::vector<CdnBuildEntry>* entries);
// Build CDN URLs for various query files.
// Base URL example: "https://cef-builds.spotifycdn.com/"
//
// Channel parameter: "" (stable, default) or "beta"
// - stable (empty): stable.txt, 137.json, 137_windows64.json
// - beta: beta.txt, 137_beta.json, 137_windows64_beta.json
// Build URL for channel milestone file (stable.txt or beta.txt)
std::string BuildChannelUrl(const std::string& base_url,
const std::string& channel);
// Legacy: equivalent to BuildChannelUrl(base_url, "")
std::string BuildStableUrl(const std::string& base_url);
// Build URL for milestone manifest with optional channel suffix
std::string BuildMilestoneUrl(const std::string& base_url,
int milestone,
const std::string& channel = "");
// Build URL for platform manifest with optional channel suffix
std::string BuildPlatformUrl(const std::string& base_url,
int milestone,
const std::string& platform,
const std::string& channel = "");
// Build URL for ABI hash manifest with optional channel suffix
std::string BuildAbiHashUrl(const std::string& base_url,
const std::string& abi_hash,
const std::string& platform,
const std::string& channel = "");
std::string BuildArchiveUrl(const std::string& base_url,
const std::string& filename);
std::string BuildHashFileUrl(const std::string& base_url,
const std::string& filename); // .sha256 file
// Find the best build entry for an app's requirements.
// Filters by abi_hash (if specified) and version range, returns newest match.
// Entries whose version appears in |skip_versions| are excluded.
std::optional<CdnBuildEntry> FindBestBuildEntry(
const std::vector<CdnBuildEntry>& entries,
const std::string& vmin,
const std::string& vmax,
const std::string& abi_hash,
const std::set<Version>& skip_versions = {});
// Build a bounded diagnostic for a failed FindBestBuildEntry() call. Reports
// the effective requirements and the checks that rejected each candidate.
std::string BuildNoMatchingCdnVersionMessage(
const std::vector<CdnBuildEntry>& entries,
const std::string& platform,
const std::string& vmin,
const std::string& vmax,
const std::string& abi_hash,
const CdnBuildExclusionReasons& exclusion_reasons = {});
// Build URL for revocation list.
// Returns: <base_url>/revoked.json
std::string BuildRevocationListUrl(const std::string& base_url);
// Convert error code to human-readable string for logging.
const char* ManifestErrorToString(ManifestError error);
} // namespace cef_installer
#endif // CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_CDN_MANIFEST_H_
@@ -0,0 +1,754 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cef/libcef_dll/bootstrap/installer/installer_cdn_manifest.h"
#include <set>
#include "testing/gtest/include/gtest/gtest.h"
namespace cef_installer {
namespace {
// ============================================================================
// ParseStableMilestone tests
// ============================================================================
TEST(InstallerCdnManifestTest, ParseStableMilestoneValid) {
int milestone = 0;
ManifestError result = ParseStableMilestone("137", &milestone);
EXPECT_EQ(ManifestError::kSuccess, result);
EXPECT_EQ(137, milestone);
}
TEST(InstallerCdnManifestTest, ParseStableMilestoneWithWhitespace) {
int milestone = 0;
ManifestError result = ParseStableMilestone(" 137\n", &milestone);
EXPECT_EQ(ManifestError::kSuccess, result);
EXPECT_EQ(137, milestone);
}
TEST(InstallerCdnManifestTest, ParseStableMilestoneEmpty) {
int milestone = 0;
ManifestError result = ParseStableMilestone("", &milestone);
EXPECT_EQ(ManifestError::kMissingRequiredField, result);
}
TEST(InstallerCdnManifestTest, ParseStableMilestoneNonNumeric) {
int milestone = 0;
ManifestError result = ParseStableMilestone("abc", &milestone);
EXPECT_EQ(ManifestError::kInvalidFieldValue, result);
}
TEST(InstallerCdnManifestTest, ParseStableMilestoneNegative) {
int milestone = 0;
ManifestError result = ParseStableMilestone("-1", &milestone);
EXPECT_EQ(ManifestError::kInvalidFieldValue, result);
}
TEST(InstallerCdnManifestTest, ParseStableMilestoneZero) {
int milestone = 0;
ManifestError result = ParseStableMilestone("0", &milestone);
EXPECT_EQ(ManifestError::kInvalidFieldValue, result);
}
// ============================================================================
// ParseMilestoneManifest tests
// ============================================================================
TEST(InstallerCdnManifestTest, ParseMilestoneManifestValid) {
const char* json = R"({
"windows64": {
"abi_hash": "abc123def4567890",
"file": "cef_binary_137.3.5_windows64_signed.tar.xz",
"last_modified": "2026-02-10T12:00:00.000000Z",
"sha1": "5a7eeda17c97c9feff93f8c196d15f3145f8127e",
"version": "137.3.5"
},
"linux64": {
"file": "cef_binary_137.3.5_linux64_signed.tar.xz",
"last_modified": "2026-02-10T12:00:00.000000Z",
"sha1": "1234567890abcdef1234567890abcdef12345678",
"version": "137.3.5"
}
})";
std::map<std::string, CdnBuildEntry> entries;
ManifestError result = ParseMilestoneManifest(json, &entries);
EXPECT_EQ(ManifestError::kSuccess, result);
ASSERT_EQ(2u, entries.size());
ASSERT_TRUE(entries.count("windows64") > 0);
EXPECT_EQ("137.3.5", entries["windows64"].version.ToString());
EXPECT_EQ("abc123def4567890", entries["windows64"].abi_hash);
EXPECT_EQ("cef_binary_137.3.5_windows64_signed.tar.xz",
entries["windows64"].file);
ASSERT_TRUE(entries.count("linux64") > 0);
EXPECT_EQ("137.3.5", entries["linux64"].version.ToString());
EXPECT_TRUE(entries["linux64"].abi_hash.empty()); // Not present in JSON
}
TEST(InstallerCdnManifestTest, ParseMilestoneManifestMissingField) {
// Missing "file" field
const char* json = R"({
"windows64": {
"sha1": "5a7eeda17c97c9feff93f8c196d15f3145f8127e",
"version": "137.3.5"
}
})";
std::map<std::string, CdnBuildEntry> entries;
ManifestError result = ParseMilestoneManifest(json, &entries);
EXPECT_EQ(ManifestError::kMissingRequiredField, result);
}
TEST(InstallerCdnManifestTest, ParseMilestoneManifestMalformed) {
const char* json = "not valid json {";
std::map<std::string, CdnBuildEntry> entries;
ManifestError result = ParseMilestoneManifest(json, &entries);
EXPECT_EQ(ManifestError::kJsonParseError, result);
}
// ============================================================================
// ParsePlatformManifest tests
// ============================================================================
TEST(InstallerCdnManifestTest, ParsePlatformManifestArray) {
const char* json = R"([
{
"abi_hash": "abc123def4567890",
"file": "cef_binary_137.3.5_windows64_signed.tar.xz",
"last_modified": "2026-02-10T12:00:00.000000Z",
"sha1": "5a7eeda17c97c9feff93f8c196d15f3145f8127e",
"version": "137.3.5"
},
{
"abi_hash": "abc123def4567890",
"file": "cef_binary_137.2.0_windows64_signed.tar.xz",
"last_modified": "2026-02-05T12:00:00.000000Z",
"sha1": "1234567890abcdef1234567890abcdef12345678",
"version": "137.2.0"
}
])";
std::vector<CdnBuildEntry> entries;
ManifestError result = ParsePlatformManifest(json, &entries);
EXPECT_EQ(ManifestError::kSuccess, result);
ASSERT_EQ(2u, entries.size());
EXPECT_EQ("137.3.5", entries[0].version.ToString());
EXPECT_EQ("137.2.0", entries[1].version.ToString());
}
TEST(InstallerCdnManifestTest, ParsePlatformManifestEmpty) {
const char* json = "[]";
std::vector<CdnBuildEntry> entries;
ManifestError result = ParsePlatformManifest(json, &entries);
EXPECT_EQ(ManifestError::kSuccess, result);
EXPECT_TRUE(entries.empty());
}
TEST(InstallerCdnManifestTest, ParsePlatformManifestMalformed) {
const char* json = "not json";
std::vector<CdnBuildEntry> entries;
ManifestError result = ParsePlatformManifest(json, &entries);
EXPECT_EQ(ManifestError::kJsonParseError, result);
}
// ============================================================================
// URL building tests
// ============================================================================
TEST(InstallerCdnManifestTest, BuildUrls) {
const std::string base = "https://cef-builds.spotifycdn.com/";
EXPECT_EQ("https://cef-builds.spotifycdn.com/stable.txt",
BuildStableUrl(base));
EXPECT_EQ("https://cef-builds.spotifycdn.com/137.json",
BuildMilestoneUrl(base, 137));
EXPECT_EQ("https://cef-builds.spotifycdn.com/137_windows64.json",
BuildPlatformUrl(base, 137, "windows64"));
EXPECT_EQ("https://cef-builds.spotifycdn.com/abc123_windows64.json",
BuildAbiHashUrl(base, "abc123", "windows64"));
EXPECT_EQ("https://cef-builds.spotifycdn.com/archive.tar.xz",
BuildArchiveUrl(base, "archive.tar.xz"));
EXPECT_EQ("https://cef-builds.spotifycdn.com/archive.tar.xz.sha256",
BuildHashFileUrl(base, "archive.tar.xz"));
EXPECT_EQ("https://cef-builds.spotifycdn.com/revoked.json",
BuildRevocationListUrl(base));
}
TEST(InstallerCdnManifestTest, BuildUrlsNoTrailingSlash) {
// Base URL without trailing slash should still work
const std::string base = "https://cef-builds.spotifycdn.com";
EXPECT_EQ("https://cef-builds.spotifycdn.com/stable.txt",
BuildStableUrl(base));
EXPECT_EQ("https://cef-builds.spotifycdn.com/137.json",
BuildMilestoneUrl(base, 137));
}
// ============================================================================
// Channel URL building tests
// ============================================================================
TEST(InstallerCdnManifestTest, BuildChannelUrlStable) {
const std::string base = "https://cef-builds.spotifycdn.com/";
// Empty channel = stable
EXPECT_EQ("https://cef-builds.spotifycdn.com/stable.txt",
BuildChannelUrl(base, ""));
}
TEST(InstallerCdnManifestTest, BuildChannelUrlBeta) {
const std::string base = "https://cef-builds.spotifycdn.com/";
EXPECT_EQ("https://cef-builds.spotifycdn.com/beta.txt",
BuildChannelUrl(base, "beta"));
}
TEST(InstallerCdnManifestTest, BuildMilestoneUrlWithChannel) {
const std::string base = "https://cef-builds.spotifycdn.com/";
// Empty channel = stable (no suffix)
EXPECT_EQ("https://cef-builds.spotifycdn.com/137.json",
BuildMilestoneUrl(base, 137, ""));
// Beta channel
EXPECT_EQ("https://cef-builds.spotifycdn.com/137_beta.json",
BuildMilestoneUrl(base, 137, "beta"));
}
TEST(InstallerCdnManifestTest, BuildPlatformUrlWithChannel) {
const std::string base = "https://cef-builds.spotifycdn.com/";
// Empty channel = stable (no suffix)
EXPECT_EQ("https://cef-builds.spotifycdn.com/137_windows64.json",
BuildPlatformUrl(base, 137, "windows64", ""));
// Beta channel
EXPECT_EQ("https://cef-builds.spotifycdn.com/137_windows64_beta.json",
BuildPlatformUrl(base, 137, "windows64", "beta"));
}
TEST(InstallerCdnManifestTest, BuildAbiHashUrlWithChannel) {
const std::string base = "https://cef-builds.spotifycdn.com/";
// Empty channel = stable (no suffix)
EXPECT_EQ("https://cef-builds.spotifycdn.com/abc123_windows64.json",
BuildAbiHashUrl(base, "abc123", "windows64", ""));
// Beta channel
EXPECT_EQ("https://cef-builds.spotifycdn.com/abc123_windows64_beta.json",
BuildAbiHashUrl(base, "abc123", "windows64", "beta"));
}
// ============================================================================
// FindBestBuildEntry tests
// ============================================================================
TEST(InstallerCdnManifestTest, FindBestBuildEntry) {
std::vector<CdnBuildEntry> entries;
CdnBuildEntry e1;
e1.version = Version::Parse("137.1.0");
e1.abi_hash = "abc123";
e1.file = "file1.tar.xz";
e1.sha1 = "hash1";
entries.push_back(e1);
CdnBuildEntry e2;
e2.version = Version::Parse("137.3.0");
e2.abi_hash = "abc123";
e2.file = "file2.tar.xz";
e2.sha1 = "hash2";
entries.push_back(e2);
CdnBuildEntry e3;
e3.version = Version::Parse("137.2.0");
e3.abi_hash = "def456"; // Different abi_hash
e3.file = "file3.tar.xz";
e3.sha1 = "hash3";
entries.push_back(e3);
// Should find newest with matching abi_hash
auto result = FindBestBuildEntry(entries, "137.0", "137.99", "abc123");
ASSERT_TRUE(result.has_value());
EXPECT_EQ("137.3.0", result->version.ToString());
EXPECT_EQ("abc123", result->abi_hash);
}
TEST(InstallerCdnManifestTest, FindBestBuildEntryEmptyAbiHash) {
std::vector<CdnBuildEntry> entries;
CdnBuildEntry e1;
e1.version = Version::Parse("137.1.0");
e1.abi_hash = "abc123";
e1.file = "file1.tar.xz";
e1.sha1 = "hash1";
entries.push_back(e1);
CdnBuildEntry e2;
e2.version = Version::Parse("137.3.0");
e2.abi_hash = "def456";
e2.file = "file2.tar.xz";
e2.sha1 = "hash2";
entries.push_back(e2);
// Empty abi_hash means match any
auto result = FindBestBuildEntry(entries, "137.0", "137.99", "");
ASSERT_TRUE(result.has_value());
EXPECT_EQ("137.3.0",
result->version.ToString()); // Newest regardless of hash
}
TEST(InstallerCdnManifestTest, FindBestBuildEntryNoMatch) {
std::vector<CdnBuildEntry> entries;
CdnBuildEntry e1;
e1.version = Version::Parse("136.1.0"); // Below range
e1.abi_hash = "abc123";
e1.file = "file1.tar.xz";
e1.sha1 = "hash1";
entries.push_back(e1);
auto result = FindBestBuildEntry(entries, "137.0", "137.99", "abc123");
EXPECT_FALSE(result.has_value());
}
TEST(InstallerCdnManifestTest, FindBestBuildEntryVersionRange) {
std::vector<CdnBuildEntry> entries;
CdnBuildEntry e1;
e1.version = Version::Parse("137.1.0");
e1.abi_hash = "abc123";
e1.file = "file1.tar.xz";
e1.sha1 = "hash1";
entries.push_back(e1);
CdnBuildEntry e2;
e2.version = Version::Parse("137.5.0"); // Above range
e2.abi_hash = "abc123";
e2.file = "file2.tar.xz";
e2.sha1 = "hash2";
entries.push_back(e2);
// Max version is 137.3
auto result = FindBestBuildEntry(entries, "137.0", "137.3", "abc123");
ASSERT_TRUE(result.has_value());
EXPECT_EQ("137.1.0", result->version.ToString()); // Only one in range
}
TEST(InstallerCdnManifestTest, FindBestBuildEntryEmptyList) {
std::vector<CdnBuildEntry> entries;
auto result = FindBestBuildEntry(entries, "137.0", "137.99", "abc123");
EXPECT_FALSE(result.has_value());
}
TEST(InstallerCdnManifestTest, FindBestBuildEntrySkipsInstalledVersions) {
std::vector<CdnBuildEntry> entries;
CdnBuildEntry e1;
e1.version = Version::Parse("137.1.0");
e1.abi_hash = "abc123";
e1.file = "file1.tar.xz";
e1.sha1 = "hash1";
entries.push_back(e1);
CdnBuildEntry e2;
e2.version = Version::Parse("137.2.0");
e2.abi_hash = "abc123";
e2.file = "file2.tar.xz";
e2.sha1 = "hash2";
entries.push_back(e2);
CdnBuildEntry e3;
e3.version = Version::Parse("137.3.0");
e3.abi_hash = "abc123";
e3.file = "file3.tar.xz";
e3.sha1 = "hash3";
entries.push_back(e3);
std::set<Version> skip;
skip.insert(e2.version);
skip.insert(e3.version);
auto result = FindBestBuildEntry(entries, "137.0", "137.99", "abc123", skip);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(e1.version, result->version);
}
TEST(InstallerCdnManifestTest, FindBestBuildEntryAllSkipped) {
std::vector<CdnBuildEntry> entries;
CdnBuildEntry e1;
e1.version = Version::Parse("137.1.0");
e1.abi_hash = "abc123";
e1.file = "file1.tar.xz";
e1.sha1 = "hash1";
entries.push_back(e1);
std::set<Version> skip;
skip.insert(e1.version);
auto result = FindBestBuildEntry(entries, "137.0", "137.99", "abc123", skip);
EXPECT_FALSE(result.has_value());
}
TEST(InstallerCdnManifestTest,
NoMatchingVersionMessageListsFailedRequirements) {
CdnBuildEntry below_and_wrong_abi;
below_and_wrong_abi.version = Version::Parse("136.1.0");
below_and_wrong_abi.abi_hash = "def456";
CdnBuildEntry above;
above.version = Version::Parse("138.1.0");
above.abi_hash = "abc123";
CdnBuildEntry excluded;
excluded.version = Version::Parse("137.2.0");
excluded.abi_hash = "abc123";
CdnBuildExclusionReasons exclusion_reasons = {
{excluded.version, {CdnBuildExclusionReason::kRevoked}}};
const std::string message = BuildNoMatchingCdnVersionMessage(
{below_and_wrong_abi, above, excluded}, "windows64", "137.0", "137.99",
"abc123", exclusion_reasons);
EXPECT_NE(std::string::npos,
message.find("platform = windows64, version >= 137.0, version <= "
"137.99, ABI hash = abc123"));
EXPECT_NE(std::string::npos,
message.find("136.1.0 [ABI hash mismatch (got def456), version "
"below vmin]"));
EXPECT_NE(std::string::npos, message.find("138.1.0 [version above vmax]"));
EXPECT_NE(std::string::npos, message.find("137.2.0 [excluded: revoked]"));
}
TEST(InstallerCdnManifestTest,
NoMatchingVersionMessageListsEachExclusionReason) {
CdnBuildEntry installed;
installed.version = Version::Parse("137.1.0");
CdnBuildEntry disqualified;
disqualified.version = Version::Parse("137.2.0");
CdnBuildEntry failed;
failed.version = Version::Parse("137.3.0");
CdnBuildExclusionReasons exclusion_reasons = {
{installed.version, {CdnBuildExclusionReason::kAlreadyInstalled}},
{disqualified.version,
{CdnBuildExclusionReason::kLaunchDisqualified,
CdnBuildExclusionReason::kRevoked}},
{failed.version, {CdnBuildExclusionReason::kPriorDownloadFailure}}};
const std::string message = BuildNoMatchingCdnVersionMessage(
{installed, disqualified, failed}, "windows64", "137.0", "", "",
exclusion_reasons);
EXPECT_NE(std::string::npos,
message.find("137.1.0 [excluded: already installed]"));
EXPECT_NE(std::string::npos,
message.find("137.2.0 [excluded: revoked, disqualified by launch "
"health]"));
EXPECT_NE(std::string::npos,
message.find("137.3.0 [excluded: prior download failure]"));
}
TEST(InstallerCdnManifestTest,
NoMatchingVersionMessageReportsMissingCandidates) {
const std::string message =
BuildNoMatchingCdnVersionMessage({}, "windows64", "151.1", "", "abc123");
EXPECT_EQ(
"No CDN version matches requirements (platform = windows64, version >= "
"151.1, ABI hash = abc123); no validated CDN manifest candidates were "
"available",
message);
}
// ============================================================================
// Security: Filename Validation Tests (M1)
// ============================================================================
TEST(InstallerCdnManifestTest, ParseBuildEntryRejectsPathTraversal) {
// Filename with ".." should be rejected
const char* json = R"({
"windows64": {
"file": "../../../evil.tar.xz",
"sha1": "5a7eeda17c97c9feff93f8c196d15f3145f8127e",
"version": "137.3.5"
}
})";
std::map<std::string, CdnBuildEntry> entries;
ManifestError result = ParseMilestoneManifest(json, &entries);
EXPECT_EQ(ManifestError::kInvalidFieldValue, result);
}
TEST(InstallerCdnManifestTest, ParseBuildEntryRejectsForwardSlash) {
// Filename with "/" should be rejected
const char* json = R"({
"windows64": {
"file": "subdir/archive.tar.xz",
"sha1": "5a7eeda17c97c9feff93f8c196d15f3145f8127e",
"version": "137.3.5"
}
})";
std::map<std::string, CdnBuildEntry> entries;
ManifestError result = ParseMilestoneManifest(json, &entries);
EXPECT_EQ(ManifestError::kInvalidFieldValue, result);
}
TEST(InstallerCdnManifestTest, ParseBuildEntryRejectsBackslash) {
// Filename with "\" should be rejected
const char* json = R"({
"windows64": {
"file": "subdir\\archive.tar.xz",
"sha1": "5a7eeda17c97c9feff93f8c196d15f3145f8127e",
"version": "137.3.5"
}
})";
std::map<std::string, CdnBuildEntry> entries;
ManifestError result = ParseMilestoneManifest(json, &entries);
EXPECT_EQ(ManifestError::kInvalidFieldValue, result);
}
TEST(InstallerCdnManifestTest, ParseBuildEntryRejectsUrlScheme) {
// Filename with ":" (URL scheme indicator) should be rejected
const char* json = R"({
"windows64": {
"file": "https://evil.com/malware.tar.xz",
"sha1": "5a7eeda17c97c9feff93f8c196d15f3145f8127e",
"version": "137.3.5"
}
})";
std::map<std::string, CdnBuildEntry> entries;
ManifestError result = ParseMilestoneManifest(json, &entries);
EXPECT_EQ(ManifestError::kInvalidFieldValue, result);
}
TEST(InstallerCdnManifestTest, ParseBuildEntryRejectsHiddenFile) {
// Filename starting with "." should be rejected
const char* json = R"({
"windows64": {
"file": ".hidden.tar.xz",
"sha1": "5a7eeda17c97c9feff93f8c196d15f3145f8127e",
"version": "137.3.5"
}
})";
std::map<std::string, CdnBuildEntry> entries;
ManifestError result = ParseMilestoneManifest(json, &entries);
EXPECT_EQ(ManifestError::kInvalidFieldValue, result);
}
TEST(InstallerCdnManifestTest, ParseBuildEntryRejectsInvalidExtension) {
// Filename without valid archive extension should be rejected
const char* json = R"({
"windows64": {
"file": "archive.exe",
"sha1": "5a7eeda17c97c9feff93f8c196d15f3145f8127e",
"version": "137.3.5"
}
})";
std::map<std::string, CdnBuildEntry> entries;
ManifestError result = ParseMilestoneManifest(json, &entries);
EXPECT_EQ(ManifestError::kInvalidFieldValue, result);
}
TEST(InstallerCdnManifestTest, ParseBuildEntryAcceptsValidExtensions) {
// Only .tar.xz is supported by the extraction code (ExtractTarXz).
const char* json_tarxz = R"({
"windows64": {
"file": "cef_binary_137.3.5.tar.xz",
"sha1": "5a7eeda17c97c9feff93f8c196d15f3145f8127e",
"version": "137.3.5"
}
})";
std::map<std::string, CdnBuildEntry> entries;
ManifestError result = ParseMilestoneManifest(json_tarxz, &entries);
EXPECT_EQ(ManifestError::kSuccess, result);
}
TEST(InstallerCdnManifestTest, ParseBuildEntryRejectsUnsupportedExtensions) {
// .7z and .zip are not supported by the extraction code.
const char* json_7z = R"({
"windows64": {
"file": "cef_binary_137.3.5.7z",
"sha1": "5a7eeda17c97c9feff93f8c196d15f3145f8127e",
"version": "137.3.5"
}
})";
std::map<std::string, CdnBuildEntry> entries;
ManifestError result = ParseMilestoneManifest(json_7z, &entries);
EXPECT_EQ(ManifestError::kInvalidFieldValue, result);
const char* json_zip = R"({
"windows64": {
"file": "cef_binary_137.3.5.zip",
"sha1": "5a7eeda17c97c9feff93f8c196d15f3145f8127e",
"version": "137.3.5"
}
})";
entries.clear();
result = ParseMilestoneManifest(json_zip, &entries);
EXPECT_EQ(ManifestError::kInvalidFieldValue, result);
}
// ============================================================================
// Security: Field Length Limit Tests (M3)
// ============================================================================
TEST(InstallerCdnManifestTest, ParseBuildEntryRejectsOverlyLongVersion) {
// Version field longer than 64 chars should be rejected
std::string long_version(100, '1'); // 100 chars
std::string json = R"({
"windows64": {
"file": "archive.tar.xz",
"sha1": "5a7eeda17c97c9feff93f8c196d15f3145f8127e",
"version": ")" +
long_version + R"("
}
})";
std::map<std::string, CdnBuildEntry> entries;
ManifestError result = ParseMilestoneManifest(json, &entries);
EXPECT_EQ(ManifestError::kInvalidFieldValue, result);
}
TEST(InstallerCdnManifestTest, ParseBuildEntryRejectsOverlyLongSha1) {
// SHA1 field longer than 40 chars should be rejected
std::string long_sha1(50, 'a'); // 50 chars (should be 40)
std::string json = R"({
"windows64": {
"file": "archive.tar.xz",
"sha1": ")" + long_sha1 +
R"(",
"version": "137.3.5"
}
})";
std::map<std::string, CdnBuildEntry> entries;
ManifestError result = ParseMilestoneManifest(json, &entries);
EXPECT_EQ(ManifestError::kInvalidFieldValue, result);
}
TEST(InstallerCdnManifestTest, ParseBuildEntryRejectsOverlyLongAbiHash) {
// abi_hash field longer than 256 chars should be rejected
std::string long_hash(300, 'x'); // 300 chars
std::string json = R"({
"windows64": {
"file": "archive.tar.xz",
"sha1": "5a7eeda17c97c9feff93f8c196d15f3145f8127e",
"version": "137.3.5",
"abi_hash": ")" +
long_hash + R"("
}
})";
std::map<std::string, CdnBuildEntry> entries;
ManifestError result = ParseMilestoneManifest(json, &entries);
EXPECT_EQ(ManifestError::kInvalidFieldValue, result);
}
TEST(InstallerCdnManifestTest, ParseBuildEntryRejectsOverlyLongTimestamp) {
// last_modified field longer than 64 chars should be rejected
std::string long_timestamp(100, 'T'); // 100 chars
std::string json = R"({
"windows64": {
"file": "archive.tar.xz",
"sha1": "5a7eeda17c97c9feff93f8c196d15f3145f8127e",
"version": "137.3.5",
"last_modified": ")" +
long_timestamp + R"("
}
})";
std::map<std::string, CdnBuildEntry> entries;
ManifestError result = ParseMilestoneManifest(json, &entries);
EXPECT_EQ(ManifestError::kInvalidFieldValue, result);
}
TEST(InstallerCdnManifestTest, ParseBuildEntryRejectsOverlyLongFilename) {
// Filename longer than 256 chars should be rejected
std::string long_name(300, 'f');
long_name += ".tar.xz"; // Add valid extension
std::string json = R"({
"windows64": {
"file": ")" + long_name +
R"(",
"sha1": "5a7eeda17c97c9feff93f8c196d15f3145f8127e",
"version": "137.3.5"
}
})";
std::map<std::string, CdnBuildEntry> entries;
ManifestError result = ParseMilestoneManifest(json, &entries);
EXPECT_EQ(ManifestError::kInvalidFieldValue, result);
}
// ============================================================================
// ManifestErrorToString tests
// ============================================================================
TEST(InstallerCdnManifestTest, ManifestErrorToString) {
EXPECT_STREQ("Success", ManifestErrorToString(ManifestError::kSuccess));
EXPECT_STREQ("JSON parse error",
ManifestErrorToString(ManifestError::kJsonParseError));
EXPECT_STREQ("Missing required field",
ManifestErrorToString(ManifestError::kMissingRequiredField));
EXPECT_STREQ("Invalid field value",
ManifestErrorToString(ManifestError::kInvalidFieldValue));
}
} // namespace
} // namespace cef_installer
@@ -0,0 +1,375 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cef/libcef_dll/bootstrap/installer/installer_config.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/json/string_escape.h"
#include "base/logging.h"
#include "base/notreached.h"
#include "base/strings/stringprintf.h"
#include "base/uuid.h"
#include "cef/include/cef_api_hash.h"
#include "cef/libcef_dll/bootstrap/installer/installer_constants.h"
#include "cef/libcef_dll/bootstrap/installer/installer_paths.h"
#include "cef/libcef_dll/bootstrap/installer/installer_policy.h"
#include "cef/libcef_dll/bootstrap/installer/installer_validation.h"
#include "cef/libcef_dll/bootstrap/installer/installer_version.h"
#include "include/wrapper/cef_util_win.h"
namespace cef_installer {
namespace {
using config_fields::kAbiHashField;
using config_fields::kAppIdField;
using config_fields::kBundledCefPathField;
using config_fields::kCdnUrlsField;
using config_fields::kChannelField;
using config_fields::kEnableExplicitModesField;
using config_fields::kInstallPathField;
using config_fields::kLaunchHealthField;
using config_fields::kUncheckedCefPathField;
using config_fields::kVmaxField;
using config_fields::kVminField;
void WarnUnrecognizedConfigKeys(const base::DictValue& dict) {
for (const auto item : dict) {
if (config_fields::IsRecognizedConfigField(item.first)) {
continue;
}
std::string escaped_key;
base::EscapeJSONString(item.first, /*put_in_quotes=*/true, &escaped_key);
LOG(WARNING) << "Unrecognized installer config key: " << escaped_key;
}
}
} // namespace
const char* LaunchHealthModeToString(LaunchHealthMode mode) {
switch (mode) {
case LaunchHealthMode::kOff:
return "off";
case LaunchHealthMode::kExplicit:
return "explicit";
case LaunchHealthMode::kExitCode:
return "exit_code";
}
NOTREACHED();
}
std::string ConfigToJson(const Config& config) {
base::DictValue dict;
dict.Set(kAppIdField, config.appid);
dict.Set(kVminField, config.vmin);
if (!config.vmax.empty()) {
dict.Set(kVmaxField, config.vmax);
}
if (!config.abi_hash.empty()) {
dict.Set(kAbiHashField, config.abi_hash);
}
if (!config.channel.empty()) {
dict.Set(kChannelField, config.channel);
}
if (!config.cdn_urls.empty()) {
base::ListValue urls;
for (const auto& url : config.cdn_urls) {
urls.Append(url);
}
dict.Set(kCdnUrlsField, std::move(urls));
}
dict.Set(kLaunchHealthField,
std::string(LaunchHealthModeToString(config.launch_health)));
if (config.enable_explicit_modes) {
dict.Set(kEnableExplicitModesField, true);
}
if (!config.unchecked_cef_path.empty()) {
dict.Set(kUncheckedCefPathField, config.unchecked_cef_path);
}
if (!config.bundled_cef_path.empty()) {
dict.Set(kBundledCefPathField, config.bundled_cef_path);
}
std::string json;
base::JSONWriter::Write(dict, &json);
return json;
}
ConfigError ParseConfigFromJson(const std::string& json,
Config* config,
const ConfigParseOptions& options,
std::string* diagnostic) {
if (diagnostic) {
diagnostic->clear();
}
if (!config) {
if (diagnostic) {
*diagnostic = "Config output is null";
}
return ConfigError::kMissingRequiredField;
}
std::optional<base::DictValue> parsed =
base::JSONReader::ReadDict(json, base::JSON_PARSE_RFC);
if (!parsed) {
if (diagnostic) {
*diagnostic = "Malformed installer config JSON";
}
return ConfigError::kJsonParseError;
}
const base::DictValue& dict = *parsed;
WarnUnrecognizedConfigKeys(dict);
// Read appid (required)
const std::string* appid = dict.FindString(kAppIdField);
if (!appid || appid->empty()) {
return ConfigError::kMissingRequiredField;
}
base::Uuid uuid = base::Uuid::ParseCaseInsensitive(*appid);
if (!uuid.is_valid()) {
return ConfigError::kInvalidFieldValue;
}
config->appid = *appid;
// Read vmin (required)
const std::string* vmin = dict.FindString(kVminField);
if (!vmin || vmin->empty()) {
return ConfigError::kMissingRequiredField;
}
Version vmin_parsed = Version::Parse(*vmin);
if (!vmin_parsed.IsValid()) {
return ConfigError::kInvalidFieldValue;
}
const std::string configured_vmin = *vmin;
config->vmin = configured_vmin;
// Clamp vmin to the bootstrap's API version - bootstrap can't use older CEF
// than it was compiled against. Cap at CEF_API_VERSION_LAST to handle
// CEF_API_VERSION being EXPERIMENTAL or NEXT (larger than any real version).
constexpr int bootstrap_api_version = (CEF_API_VERSION < CEF_API_VERSION_LAST)
? CEF_API_VERSION
: CEF_API_VERSION_LAST;
Version api_min = Version::FromApiVersion(bootstrap_api_version);
if (api_min.IsValid() && api_min > vmin_parsed) {
LOG(WARNING) << "Config vmin " << *vmin << " clamped to "
<< api_min.ToString() << " (bootstrap API version)";
config->vmin = api_min.ToString();
}
// Read vmax (optional)
const std::string* vmax = dict.FindString(kVmaxField);
if (vmax && !vmax->empty()) {
Version vmax_parsed = Version::Parse(*vmax);
if (!vmax_parsed.IsValid()) {
return ConfigError::kInvalidFieldValue;
}
config->vmax = *vmax;
Version effective_vmin = Version::Parse(config->vmin);
if (effective_vmin > vmax_parsed) {
if (diagnostic) {
if (vmin_parsed > vmax_parsed) {
*diagnostic = base::StringPrintf(
"Configured vmin %s exceeds configured vmax %s",
configured_vmin.c_str(), vmax->c_str());
} else {
*diagnostic = base::StringPrintf(
"Configured vmin %s has effective vmin %s after clamping to "
"bootstrap API version %d, which exceeds configured vmax %s",
configured_vmin.c_str(), config->vmin.c_str(),
bootstrap_api_version, vmax->c_str());
}
}
return ConfigError::kInvalidVersionRange;
}
} else {
config->vmax.clear();
}
// Read abi_hash (optional, must be hex if present)
const std::string* abi_hash = dict.FindString(kAbiHashField);
if (abi_hash && !abi_hash->empty()) {
if (!IsValidAbiHash(*abi_hash)) {
return ConfigError::kInvalidFieldValue;
}
config->abi_hash = *abi_hash;
} else {
config->abi_hash.clear();
}
// Read channel (optional, empty = stable)
const std::string* channel = dict.FindString(kChannelField);
if (channel && !channel->empty()) {
// Only "beta" is a valid non-default channel
if (*channel != kChannelBeta) {
return ConfigError::kInvalidFieldValue;
}
config->channel = *channel;
} else {
config->channel.clear(); // Empty = stable (default)
}
const base::Value* cdn_urls = dict.Find(kCdnUrlsField);
if (!cdn_urls) {
config->cdn_urls.clear();
} else if (!cdn_urls->is_list()) {
config->cdn_urls.clear();
if (diagnostic) {
*diagnostic = "cdn_urls must be an array";
}
return ConfigError::kInvalidFieldValue;
} else {
std::vector<std::string> values;
values.reserve(cdn_urls->GetList().size());
for (const auto& value : cdn_urls->GetList()) {
if (!value.is_string()) {
config->cdn_urls.clear();
if (diagnostic) {
*diagnostic = "cdn_urls entries must be strings";
}
return ConfigError::kInvalidFieldValue;
}
values.push_back(value.GetString());
}
if (!ValidateAndNormalizeCdnUrls(values, &config->cdn_urls, diagnostic)) {
config->cdn_urls.clear();
return ConfigError::kInvalidFieldValue;
}
}
// Launch health is a trusted core field in every Config source. Missing
// legacy configs default to off; present values must use canonical strings.
const base::Value* launch_health = dict.Find(kLaunchHealthField);
if (!launch_health) {
config->launch_health = LaunchHealthMode::kOff;
} else if (!launch_health->is_string()) {
if (diagnostic) {
*diagnostic = "launch_health must be a string";
}
return ConfigError::kInvalidFieldValue;
} else if (launch_health->GetString() == "off") {
config->launch_health = LaunchHealthMode::kOff;
} else if (launch_health->GetString() == "explicit") {
config->launch_health = LaunchHealthMode::kExplicit;
} else if (launch_health->GetString() == "exit_code") {
config->launch_health = LaunchHealthMode::kExitCode;
} else {
if (diagnostic) {
*diagnostic = "launch_health must be one of: off, explicit, exit_code";
}
return ConfigError::kInvalidFieldValue;
}
// Only authoritative in the bootstrap's own embedded resource.
// Ignored in client DLL resources and RunInstaller JSON.
if (options.allow_enable_explicit_modes) {
if (std::optional<bool> val = dict.FindBool(kEnableExplicitModesField)) {
config->enable_explicit_modes = *val;
} else {
config->enable_explicit_modes = false;
}
} else {
config->enable_explicit_modes = false;
}
// Only from client DLL resource or RunInstaller JSON. Ignored in the
// bootstrap resource, which has no client DLL directory to resolve relative
// paths against.
if (options.allow_unchecked_cef_path) {
const std::string* unchecked_path = dict.FindString(kUncheckedCefPathField);
if (unchecked_path && !unchecked_path->empty()) {
config->unchecked_cef_path = *unchecked_path;
} else {
config->unchecked_cef_path.clear();
}
} else {
config->unchecked_cef_path.clear();
}
// Only from client DLL resource or RunInstaller JSON. Same gating rationale
// as unchecked_cef_path: relative paths need a client DLL directory.
if (options.allow_bundled_cef_path) {
const std::string* bundled_path = dict.FindString(kBundledCefPathField);
if (bundled_path && !bundled_path->empty()) {
config->bundled_cef_path = *bundled_path;
} else {
config->bundled_cef_path.clear();
}
} else {
config->bundled_cef_path.clear();
}
// Only the trusted client DLL resource may select the automatic installer
// namespace. Bootstrap resources cannot select a write location.
// RunInstaller JSON is parsed independently into ExtendedConfig.
if (options.allow_install_path) {
const base::Value* install_path = dict.Find(kInstallPathField);
if (!install_path) {
config->install_path.clear();
} else if (!install_path->is_string()) {
config->install_path.clear();
if (diagnostic) {
*diagnostic = "install_path must be a string";
}
return ConfigError::kInvalidFieldValue;
} else {
config->install_path = install_path->GetString();
}
} else {
config->install_path.clear();
}
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
// Read certificate_thumbprint (optional, test-only).
const std::string* thumbprint =
dict.FindString(config_fields::kCertificateThumbprintField);
if (thumbprint && !thumbprint->empty()) {
config->certificate_thumbprint = *thumbprint;
} else {
config->certificate_thumbprint.clear();
}
#endif
return ConfigError::kSuccess;
}
ConfigError ReadConfigFromResource(HMODULE module,
Config* config,
const ConfigParseOptions& options,
std::string* diagnostic) {
if (!config) {
return ConfigError::kMissingRequiredField;
}
std::string json;
if (!cef_util::ReadResourceData(module, kConfigResourceName, &json)) {
if (diagnostic) {
*diagnostic = "Embedded installer config resource not found";
}
return ConfigError::kResourceNotFound;
}
return ParseConfigFromJson(json, config, options, diagnostic);
}
const char* ConfigErrorToString(ConfigError error) {
switch (error) {
case ConfigError::kSuccess:
return "Success";
case ConfigError::kJsonParseError:
return "Malformed JSON";
case ConfigError::kMissingRequiredField:
return "Required field missing";
case ConfigError::kInvalidFieldValue:
return "Invalid field value";
case ConfigError::kInvalidVersionRange:
return "Invalid version range";
case ConfigError::kResourceNotFound:
return "Embedded resource not found";
}
return "Unknown error";
}
} // namespace cef_installer
@@ -0,0 +1,216 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_CONFIG_H_
#define CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_CONFIG_H_
#include <windows.h>
#include <array>
#include <string>
#include <string_view>
#include <vector>
namespace cef_installer {
namespace config_fields {
inline constexpr char kAppIdField[] = "appid";
inline constexpr char kVminField[] = "vmin";
inline constexpr char kVmaxField[] = "vmax";
inline constexpr char kAbiHashField[] = "abi_hash";
inline constexpr char kChannelField[] = "channel";
inline constexpr char kCdnUrlsField[] = "cdn_urls";
inline constexpr char kLaunchHealthField[] = "launch_health";
inline constexpr char kEnableExplicitModesField[] = "enable_explicit_modes";
inline constexpr char kUncheckedCefPathField[] = "unchecked_cef_path";
inline constexpr char kBundledCefPathField[] = "bundled_cef_path";
inline constexpr char kInstallPathField[] = "install_path";
inline constexpr char kCertificateThumbprintField[] = "certificate_thumbprint";
inline constexpr char kForceCheckField[] = "force_check";
inline constexpr char kShowProgressUiField[] = "show_progress_ui";
inline constexpr char kParentWindowField[] = "parent_window";
inline constexpr char kLocalDownloadPathField[] = "local_download_path";
inline constexpr char kLogLevelField[] = "log_level";
inline constexpr char kDownloadTimeoutMsField[] = "download_timeout_ms";
inline constexpr char kMaxAgeDaysField[] = "max_age_days";
inline constexpr std::array<std::string_view, 11> kApplicationConfigFields = {
kAppIdField,
kVminField,
kVmaxField,
kAbiHashField,
kChannelField,
kCdnUrlsField,
kLaunchHealthField,
kEnableExplicitModesField,
kUncheckedCefPathField,
kBundledCefPathField,
kInstallPathField,
};
inline constexpr std::array<std::string_view, 11> kExtendedConfigFields = {
kCdnUrlsField, kInstallPathField,
kBundledCefPathField, kCertificateThumbprintField,
kForceCheckField, kShowProgressUiField,
kParentWindowField, kLocalDownloadPathField,
kLogLevelField, kDownloadTimeoutMsField,
kMaxAgeDaysField,
};
constexpr bool IsRecognizedConfigField(std::string_view key) {
for (const std::string_view field : kApplicationConfigFields) {
if (field == key) {
return true;
}
}
for (const std::string_view field : kExtendedConfigFields) {
if (field == key) {
return true;
}
}
return false;
}
} // namespace config_fields
// Controls local launch-health tracking for this application.
enum class LaunchHealthMode {
kOff,
kExplicit,
kExitCode,
};
const char* LaunchHealthModeToString(LaunchHealthMode mode);
// Controls which optional fields are parsed from config JSON. Each flag
// corresponds to a field that is only meaningful in certain contexts (e.g.,
// client DLL resource vs. bootstrap resource or operation API). When a flag is
// false, the corresponding Config field retains its default value regardless
// of what the JSON contains.
//
// SECURITY: install_path is accepted only from the trusted client DLL resource
// as an application-owned namespace selector. Bootstrap resources cannot
// select it. CDN URL lists are accepted from every selected application config
// source. Command-line overrides for sensitive fields are compiled out of
// official release builds entirely.
struct ConfigParseOptions {
bool allow_enable_explicit_modes = false;
bool allow_unchecked_cef_path = false;
bool allow_bundled_cef_path = false;
bool allow_install_path = false;
};
// Application configuration loaded from an embedded resource.
struct Config {
std::string appid; // UUID - unique identifier that never changes
std::string vmin; // Minimum version (required, e.g., "137.1")
std::string vmax; // Maximum version (optional, e.g., "137.99")
std::string abi_hash; // Sandbox compatibility hash (16-char hex)
std::string channel; // Release channel: "" (stable, default) or "beta"
// Ordered application CDN base URLs. Empty means the effective-source
// resolver supplies the hardcoded default after considering policy and
// operation-specific sources.
std::vector<std::string> cdn_urls;
// Local launch-health tracking is opt-in. kExplicit is recommended for
// applications that can reliably call RunInstaller("launch_success");
// kExitCode preserves the legacy exit-code classification behavior.
LaunchHealthMode launch_health = LaunchHealthMode::kOff;
// Enable standalone installer features. When true in the bootstrap's embedded
// config:
// 1. Allows explicit /cef-update and /cef-uninstall commands in standalone
// mode
// (no client DLL) and when a client DLL is present.
// 2. Allows auto-install when no client DLL is present.
//
// When false (default), explicit commands and standalone auto-install are
// blocked in official builds. The bootstrap can only function as a launcher
// when a client DLL is present.
//
// This prevents signed bootstrap binaries from being repurposed as
// arbitrary CEF installers. In non-official builds all standalone operations
// are allowed regardless of this flag (for development/testing).
bool enable_explicit_modes = false;
// Path to a directory containing libcef.dll, used as-is without any version,
// ABI, platform, or signature checks. When set and libcef.dll exists at the
// path, the installer returns immediately without contacting the CDN or
// scanning the shared install directory.
//
// Relative paths are resolved against the directory containing the module
// that provided this config (the client DLL in bootstrap mode). For example,
// "." means "libcef.dll is next to the client DLL."
//
// This is intended for applications that bundle CEF alongside their own
// binaries and want to skip the installer entirely. The application is
// responsible for the integrity of the DLL at this path.
//
// If libcef.dll is not found at the path, the installer falls through to the
// normal resolution flow (shared directory, CDN download).
std::string unchecked_cef_path;
// Path to a directory containing a full CEF distribution (cef_version.json,
// catalog.cat, Release/libcef.dll). Participates in version selection
// (newer wins, installed wins ties, revocation demotion). Relative paths are
// resolved against the directory containing the module that provided this
// config (the client DLL in bootstrap mode).
//
// Only read from the client DLL's embedded resource or RunInstaller JSON.
// Ignored in the bootstrap resource.
std::string bundled_cef_path;
// Exclusive installer state namespace selected by the client DLL resource.
// Relative paths are resolved against the client DLL directory before being
// propagated to ExtendedConfig. Ignored in bootstrap resources;
// RunInstaller JSON has an independent ExtendedConfig field.
std::string install_path;
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
// Test-only: override the certificate thumbprint used for archive
// verification. ApplyConfigThumbprintOverride() copies this into
// ExtendedConfig::certificate_thumbprint and auto-enables signature testing
// mode so self-signed test certs are accepted. Compiled out of official
// builds.
std::string certificate_thumbprint;
#endif
};
// Error codes for configuration parsing
enum class ConfigError {
kSuccess,
kJsonParseError, // Malformed JSON
kMissingRequiredField, // appid or vmin missing
kInvalidFieldValue, // Field present but invalid format
kInvalidVersionRange, // Effective vmin is greater than vmax
kResourceNotFound, // Embedded resource not found
};
// Serialize Config to JSON for prepared-uninstall configuration binding.
std::string ConfigToJson(const Config& config);
// Parse JSON config string directly. |options| controls which context-specific
// fields are parsed; see ConfigParseOptions.
ConfigError ParseConfigFromJson(const std::string& json,
Config* config,
const ConfigParseOptions& options = {},
std::string* diagnostic = nullptr);
// Read from embedded Windows resource (RT_RCDATA with string name).
// Resource must be embedded in the client executable's .rc file:
// CEF_INSTALLER_CONFIG RCDATA "installer_config.json"
// |options| is forwarded to ParseConfigFromJson.
ConfigError ReadConfigFromResource(HMODULE module,
Config* config,
const ConfigParseOptions& options = {},
std::string* diagnostic = nullptr);
// Convert error code to human-readable string for logging
const char* ConfigErrorToString(ConfigError error);
} // namespace cef_installer
#endif // CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_CONFIG_H_
@@ -0,0 +1,892 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cef/libcef_dll/bootstrap/installer/installer_config.h"
#include <windows.h>
#include "base/check.h"
#include "base/json/json_writer.h"
#include "base/test/mock_log.h"
#include "cef/include/cef_api_hash.h"
#include "cef/libcef_dll/bootstrap/installer/installer_policy.h"
#include "cef/libcef_dll/bootstrap/installer/installer_version.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace cef_installer {
namespace {
class InstallerConfigTest : public testing::Test {};
ConfigError ParseConfigWithCdnUrls(const std::vector<std::string>& urls,
Config* config,
std::string* diagnostic = nullptr) {
base::DictValue dict;
dict.Set("appid", "12345678-1234-1234-1234-123456789abc");
dict.Set("vmin", Version::FromApiVersion(CEF_API_VERSION_LAST).ToString());
base::ListValue list;
for (const auto& url : urls) {
list.Append(url);
}
dict.Set("cdn_urls", std::move(list));
std::string json;
CHECK(base::JSONWriter::Write(dict, &json));
return ParseConfigFromJson(json, config, {}, diagnostic);
}
TEST_F(InstallerConfigTest, ParseValidJson) {
// Use vmin based on CEF_API_VERSION_LAST so it won't be clamped
Version next_ver = Version::FromApiVersion(CEF_API_VERSION_LAST);
std::string vmin_str = next_ver.ToString();
std::string json =
R"({"appid": "12345678-1234-1234-1234-123456789abc", "vmin": ")" +
vmin_str + R"(", "vmax": ")" + vmin_str +
R"(.99", "abi_hash": "abc123def4567890"})";
Config config;
ConfigError error = ParseConfigFromJson(json, &config);
EXPECT_EQ(ConfigError::kSuccess, error);
EXPECT_EQ("12345678-1234-1234-1234-123456789abc", config.appid);
EXPECT_EQ(vmin_str, config.vmin);
EXPECT_EQ(vmin_str + ".99", config.vmax);
EXPECT_EQ("abc123def4567890", config.abi_hash);
}
TEST_F(InstallerConfigTest, ParseOptionalVmax) {
// Use vmin based on CEF_API_VERSION_LAST so it won't be clamped
Version next_ver = Version::FromApiVersion(CEF_API_VERSION_LAST);
std::string vmin_str = next_ver.ToString();
std::string json =
R"({"appid": "12345678-1234-1234-1234-123456789abc", "vmin": ")" +
vmin_str + R"("})";
Config config;
ConfigError error = ParseConfigFromJson(json, &config);
EXPECT_EQ(ConfigError::kSuccess, error);
EXPECT_EQ(vmin_str, config.vmin);
EXPECT_TRUE(config.vmax.empty());
}
TEST_F(InstallerConfigTest, CdnUrlsPreserveOrderDuplicatesAndNormalize) {
Config config;
ASSERT_EQ(ConfigError::kSuccess,
ParseConfigWithCdnUrls(
{"https://one.example/prefix", "https://two.example/",
"https://one.example/prefix"},
&config));
EXPECT_EQ((std::vector<std::string>{"https://one.example/prefix/",
"https://two.example/",
"https://one.example/prefix/"}),
config.cdn_urls);
}
TEST_F(InstallerConfigTest, CdnUrlsRejectShapeAndCount) {
const std::string vmin =
Version::FromApiVersion(CEF_API_VERSION_LAST).ToString();
const std::string prefix =
R"({"appid":"12345678-1234-1234-1234-123456789abc","vmin":")" + vmin +
R"(","cdn_urls":)";
for (
const char* value :
{"null", R"("https://one.example/")", "{}", "[]",
R"(["https://one.example/",1])",
R"(["https://1.example/","https://2.example/","https://3.example/","https://4.example/"])"}) {
Config config;
std::string diagnostic;
EXPECT_EQ(
ConfigError::kInvalidFieldValue,
ParseConfigFromJson(prefix + value + "}", &config, {}, &diagnostic))
<< value;
EXPECT_TRUE(config.cdn_urls.empty()) << value;
EXPECT_FALSE(diagnostic.empty()) << value;
}
}
TEST_F(InstallerConfigTest, CdnUrlsRejectInvalidPublicUrls) {
const std::vector<std::string> invalid = {
"",
"http://example.com/",
"https://",
"https://user@example.com/",
"https://example.com/?q=1",
"https://example.com/#fragment",
"https://example.com/../bad",
"https://example.com\\bad",
"https://example.com/line\n",
"https://example.com/" + std::string(kMaxCdnUrlBytes, 'a')};
for (const auto& url : invalid) {
Config config;
std::string diagnostic;
EXPECT_EQ(ConfigError::kInvalidFieldValue,
ParseConfigWithCdnUrls({url}, &config, &diagnostic))
<< url;
EXPECT_TRUE(config.cdn_urls.empty()) << url;
EXPECT_FALSE(diagnostic.empty()) << url;
}
}
TEST_F(InstallerConfigTest, CdnUrlsAbsentAndInvalidClearReusedObject) {
Config config;
ASSERT_EQ(ConfigError::kSuccess,
ParseConfigWithCdnUrls({"https://one.example/"}, &config));
ASSERT_FALSE(config.cdn_urls.empty());
const std::string vmin =
Version::FromApiVersion(CEF_API_VERSION_LAST).ToString();
ASSERT_EQ(ConfigError::kSuccess,
ParseConfigFromJson(
R"({"appid":"12345678-1234-1234-1234-123456789abc","vmin":")" +
vmin + R"("})",
&config));
EXPECT_TRUE(config.cdn_urls.empty());
ASSERT_EQ(ConfigError::kInvalidFieldValue,
ParseConfigWithCdnUrls({}, &config));
EXPECT_TRUE(config.cdn_urls.empty());
}
TEST_F(InstallerConfigTest, LaunchHealthDefaultsOff) {
const std::string vmin =
Version::FromApiVersion(CEF_API_VERSION_LAST).ToString();
const std::string json =
R"({"appid":"12345678-1234-1234-1234-123456789abc","vmin":")" + vmin +
R"("})";
Config config;
ASSERT_EQ(ConfigError::kSuccess, ParseConfigFromJson(json, &config));
EXPECT_EQ(LaunchHealthMode::kOff, config.launch_health);
}
TEST_F(InstallerConfigTest, ParsesLaunchHealthModes) {
const std::string vmin =
Version::FromApiVersion(CEF_API_VERSION_LAST).ToString();
for (const auto& [value, expected] :
std::to_array<std::pair<const char*, LaunchHealthMode>>({
{"off", LaunchHealthMode::kOff},
{"explicit", LaunchHealthMode::kExplicit},
{"exit_code", LaunchHealthMode::kExitCode},
})) {
const std::string json =
R"({"appid":"12345678-1234-1234-1234-123456789abc","vmin":")" + vmin +
R"(","launch_health":")" + value + R"("})";
Config config;
ASSERT_EQ(ConfigError::kSuccess, ParseConfigFromJson(json, &config))
<< value;
EXPECT_EQ(expected, config.launch_health) << value;
}
}
TEST_F(InstallerConfigTest, RejectsInvalidLaunchHealth) {
const std::string vmin =
Version::FromApiVersion(CEF_API_VERSION_LAST).ToString();
for (const char* value :
{"null", "true", R"("Explicit")", R"("")", R"(" exit_code ")"}) {
const std::string json =
R"({"appid":"12345678-1234-1234-1234-123456789abc","vmin":")" + vmin +
R"(","launch_health":)" + value + "}";
Config config;
std::string diagnostic;
EXPECT_EQ(ConfigError::kInvalidFieldValue,
ParseConfigFromJson(json, &config, {}, &diagnostic))
<< value;
EXPECT_FALSE(diagnostic.empty()) << value;
}
}
TEST_F(InstallerConfigTest, ParseVminClampedToApiVersion) {
// vmin lower than bootstrap API version should be clamped up
const char* json = R"({
"appid": "12345678-1234-1234-1234-123456789abc",
"vmin": "100.0"
})";
Config config;
ConfigError error = ParseConfigFromJson(json, &config);
EXPECT_EQ(ConfigError::kSuccess, error);
// vmin should be clamped to the bootstrap's API version
constexpr int bootstrap_api_version = (CEF_API_VERSION < CEF_API_VERSION_LAST)
? CEF_API_VERSION
: CEF_API_VERSION_LAST;
Version expected_vmin = Version::FromApiVersion(bootstrap_api_version);
EXPECT_EQ(expected_vmin.ToString(), config.vmin);
}
TEST_F(InstallerConfigTest, VersionRangeValidAfterClamping) {
constexpr int bootstrap_api_version = (CEF_API_VERSION < CEF_API_VERSION_LAST)
? CEF_API_VERSION
: CEF_API_VERSION_LAST;
const std::string effective =
Version::FromApiVersion(bootstrap_api_version).ToString();
const std::string json =
R"({"appid":"12345678-1234-1234-1234-123456789abc","vmin":"1.0","vmax":")" +
effective + R"(.99"})";
Config config;
EXPECT_EQ(ConfigError::kSuccess, ParseConfigFromJson(json, &config));
EXPECT_EQ(effective, config.vmin);
}
TEST_F(InstallerConfigTest, VersionRangeEmptyAfterClamping) {
constexpr int bootstrap_api_version = (CEF_API_VERSION < CEF_API_VERSION_LAST)
? CEF_API_VERSION
: CEF_API_VERSION_LAST;
const std::string effective =
Version::FromApiVersion(bootstrap_api_version).ToString();
const std::string json =
R"({"appid":"12345678-1234-1234-1234-123456789abc","vmin":"1.0","vmax":"2.0"})";
Config config;
std::string diagnostic;
EXPECT_EQ(ConfigError::kInvalidVersionRange,
ParseConfigFromJson(json, &config, {}, &diagnostic));
EXPECT_NE(std::string::npos, diagnostic.find("Configured vmin 1.0"));
EXPECT_NE(std::string::npos, diagnostic.find("effective vmin " + effective));
EXPECT_NE(std::string::npos,
diagnostic.find("bootstrap API version " +
std::to_string(bootstrap_api_version)));
EXPECT_NE(std::string::npos, diagnostic.find("configured vmax 2.0"));
}
TEST_F(InstallerConfigTest, VersionRangeInvalidAsConfigured) {
const std::string json =
R"({"appid":"12345678-1234-1234-1234-123456789abc","vmin":"999.0","vmax":"998.0"})";
Config config;
std::string diagnostic;
EXPECT_EQ(ConfigError::kInvalidVersionRange,
ParseConfigFromJson(json, &config, {}, &diagnostic));
EXPECT_EQ("Configured vmin 999.0 exceeds configured vmax 998.0", diagnostic);
}
TEST_F(InstallerConfigTest, VersionRangeEqualAfterClamping) {
constexpr int bootstrap_api_version = (CEF_API_VERSION < CEF_API_VERSION_LAST)
? CEF_API_VERSION
: CEF_API_VERSION_LAST;
const std::string effective =
Version::FromApiVersion(bootstrap_api_version).ToString();
const std::string json =
R"({"appid":"12345678-1234-1234-1234-123456789abc","vmin":"1.0","vmax":")" +
effective + R"("})";
Config config;
EXPECT_EQ(ConfigError::kSuccess, ParseConfigFromJson(json, &config));
EXPECT_EQ(effective, config.vmax);
}
TEST_F(InstallerConfigTest, ParseInvalidJson) {
const char* json = "not valid json {{{";
Config config;
ConfigError error = ParseConfigFromJson(json, &config);
EXPECT_EQ(ConfigError::kJsonParseError, error);
}
TEST_F(InstallerConfigTest, ParseMissingAppid) {
const char* json = R"({
"vmin": "137.1",
"vmax": "137.99"
})";
Config config;
ConfigError error = ParseConfigFromJson(json, &config);
EXPECT_EQ(ConfigError::kMissingRequiredField, error);
}
TEST_F(InstallerConfigTest, ParseMissingVmin) {
const char* json = R"({
"appid": "12345678-1234-1234-1234-123456789abc",
"vmax": "137.99"
})";
Config config;
ConfigError error = ParseConfigFromJson(json, &config);
EXPECT_EQ(ConfigError::kMissingRequiredField, error);
}
TEST_F(InstallerConfigTest, ParseInvalidUuid) {
const char* json = R"({
"appid": "not-a-valid-uuid",
"vmin": "137.1"
})";
Config config;
ConfigError error = ParseConfigFromJson(json, &config);
EXPECT_EQ(ConfigError::kInvalidFieldValue, error);
}
TEST_F(InstallerConfigTest, ParseUnknownFieldsPreserved) {
// Unknown fields warn but remain non-fatal for forward compatibility.
// Use vmin based on CEF_API_VERSION_LAST so it won't be clamped
Version next_ver = Version::FromApiVersion(CEF_API_VERSION_LAST);
std::string vmin_str = next_ver.ToString();
std::string json =
R"({"appid": "12345678-1234-1234-1234-123456789abc", "vmin": ")" +
vmin_str + R"(", "unknown_field": "some value", "another_unknown": 42})";
base::test::MockLog log;
EXPECT_CALL(log, Log(::logging::LOGGING_WARNING, testing::_, testing::_,
testing::_, testing::HasSubstr("unknown_field")))
.WillOnce(testing::Return(true));
EXPECT_CALL(log, Log(::logging::LOGGING_WARNING, testing::_, testing::_,
testing::_, testing::HasSubstr("another_unknown")))
.WillOnce(testing::Return(true));
log.StartCapturingLogs();
Config config;
ConfigError error = ParseConfigFromJson(json, &config);
EXPECT_EQ(ConfigError::kSuccess, error);
EXPECT_EQ("12345678-1234-1234-1234-123456789abc", config.appid);
EXPECT_EQ(vmin_str, config.vmin);
}
TEST_F(InstallerConfigTest, ParseCombinedConfigFieldsWithoutWarnings) {
const std::string version =
Version::FromApiVersion(CEF_API_VERSION_LAST).ToString();
base::DictValue dict;
dict.Set(config_fields::kAppIdField, "12345678-1234-1234-1234-123456789abc");
dict.Set(config_fields::kVminField, version);
dict.Set(config_fields::kVmaxField, version);
dict.Set(config_fields::kAbiHashField, "abc123def4567890");
dict.Set(config_fields::kChannelField, "beta");
base::ListValue cdn_urls;
cdn_urls.Append("https://example.test/cef/");
dict.Set(config_fields::kCdnUrlsField, std::move(cdn_urls));
dict.Set(config_fields::kLaunchHealthField, "explicit");
dict.Set(config_fields::kEnableExplicitModesField, true);
dict.Set(config_fields::kUncheckedCefPathField, "C:\\unchecked");
dict.Set(config_fields::kBundledCefPathField, "C:\\bundled");
dict.Set(config_fields::kInstallPathField, "C:\\install");
dict.Set(config_fields::kCertificateThumbprintField, "thumbprint");
dict.Set(config_fields::kForceCheckField, true);
dict.Set(config_fields::kShowProgressUiField, false);
dict.Set(config_fields::kParentWindowField, "0");
dict.Set(config_fields::kLocalDownloadPathField, "C:\\mirror");
dict.Set(config_fields::kLogLevelField, "warning");
dict.Set(config_fields::kDownloadTimeoutMsField, 1000);
dict.Set(config_fields::kMaxAgeDaysField, 90);
std::string json;
ASSERT_TRUE(base::JSONWriter::Write(dict, &json));
base::test::MockLog log;
EXPECT_CALL(log, Log(::logging::LOGGING_WARNING, testing::_, testing::_,
testing::_, testing::_))
.Times(0);
log.StartCapturingLogs();
ConfigParseOptions options;
options.allow_enable_explicit_modes = true;
options.allow_unchecked_cef_path = true;
options.allow_bundled_cef_path = true;
options.allow_install_path = true;
Config config;
EXPECT_EQ(ConfigError::kSuccess, ParseConfigFromJson(json, &config, options));
}
// ============================================================================
// Channel Tests
// ============================================================================
TEST_F(InstallerConfigTest, ParseChannelBeta) {
const char* json = R"({
"appid": "12345678-1234-1234-1234-123456789abc",
"vmin": "137.1",
"channel": "beta"
})";
Config config;
ConfigError error = ParseConfigFromJson(json, &config);
EXPECT_EQ(ConfigError::kSuccess, error);
EXPECT_EQ("beta", config.channel);
}
TEST_F(InstallerConfigTest, ParseChannelEmptyDefault) {
const char* json = R"({
"appid": "12345678-1234-1234-1234-123456789abc",
"vmin": "137.1"
})";
Config config;
ConfigError error = ParseConfigFromJson(json, &config);
EXPECT_EQ(ConfigError::kSuccess, error);
EXPECT_TRUE(config.channel.empty()); // Empty = stable
}
TEST_F(InstallerConfigTest, ParseChannelInvalid) {
const char* json = R"({
"appid": "12345678-1234-1234-1234-123456789abc",
"vmin": "137.1",
"channel": "canary"
})";
Config config;
ConfigError error = ParseConfigFromJson(json, &config);
EXPECT_EQ(ConfigError::kInvalidFieldValue, error);
}
TEST_F(InstallerConfigTest, ParseChannelStableExplicitInvalid) {
// "stable" is not a valid explicit value - use empty string instead
const char* json = R"({
"appid": "12345678-1234-1234-1234-123456789abc",
"vmin": "137.1",
"channel": "stable"
})";
Config config;
ConfigError error = ParseConfigFromJson(json, &config);
EXPECT_EQ(ConfigError::kInvalidFieldValue, error);
}
// ============================================================================
// enable_explicit_modes Tests
// ============================================================================
TEST_F(InstallerConfigTest, ParseEnableExplicitModesTrue) {
const char* json = R"({
"appid": "12345678-1234-1234-1234-123456789abc",
"vmin": "137.1",
"enable_explicit_modes": true
})";
Config config;
ConfigError error =
ParseConfigFromJson(json, &config, {.allow_enable_explicit_modes = true});
EXPECT_EQ(ConfigError::kSuccess, error);
EXPECT_TRUE(config.enable_explicit_modes);
}
TEST_F(InstallerConfigTest, ParseEnableExplicitModesFalse) {
const char* json = R"({
"appid": "12345678-1234-1234-1234-123456789abc",
"vmin": "137.1",
"enable_explicit_modes": false
})";
Config config;
ConfigError error =
ParseConfigFromJson(json, &config, {.allow_enable_explicit_modes = true});
EXPECT_EQ(ConfigError::kSuccess, error);
EXPECT_FALSE(config.enable_explicit_modes);
}
TEST_F(InstallerConfigTest, ParseEnableExplicitModesDefault) {
// Absent field defaults to false even when allowed
const char* json = R"({
"appid": "12345678-1234-1234-1234-123456789abc",
"vmin": "137.1"
})";
Config config;
ConfigError error =
ParseConfigFromJson(json, &config, {.allow_enable_explicit_modes = true});
EXPECT_EQ(ConfigError::kSuccess, error);
EXPECT_FALSE(config.enable_explicit_modes);
}
TEST_F(InstallerConfigTest, ParseEnableExplicitModesIgnoredByDefault) {
// When allow_enable_explicit_modes is false (default), the field is ignored
const char* json = R"({
"appid": "12345678-1234-1234-1234-123456789abc",
"vmin": "137.1",
"enable_explicit_modes": true
})";
Config config;
ConfigError error = ParseConfigFromJson(json, &config);
EXPECT_EQ(ConfigError::kSuccess, error);
EXPECT_FALSE(config.enable_explicit_modes);
}
// ============================================================================
// unchecked_cef_path Tests
// ============================================================================
TEST_F(InstallerConfigTest, ParseUncheckedCefPath) {
const char* json = R"({
"appid": "12345678-1234-1234-1234-123456789abc",
"vmin": "137.1",
"unchecked_cef_path": "."
})";
Config config;
ConfigError error =
ParseConfigFromJson(json, &config, {.allow_unchecked_cef_path = true});
EXPECT_EQ(ConfigError::kSuccess, error);
EXPECT_EQ(".", config.unchecked_cef_path);
}
TEST_F(InstallerConfigTest, ParseUncheckedCefPathAbsolute) {
const char* json = R"({
"appid": "12345678-1234-1234-1234-123456789abc",
"vmin": "137.1",
"unchecked_cef_path": "C:\\App\\CEF"
})";
Config config;
ConfigError error =
ParseConfigFromJson(json, &config, {.allow_unchecked_cef_path = true});
EXPECT_EQ(ConfigError::kSuccess, error);
EXPECT_EQ("C:\\App\\CEF", config.unchecked_cef_path);
}
TEST_F(InstallerConfigTest, ParseUncheckedCefPathEmptyIgnored) {
const char* json = R"({
"appid": "12345678-1234-1234-1234-123456789abc",
"vmin": "137.1",
"unchecked_cef_path": ""
})";
Config config;
ConfigError error =
ParseConfigFromJson(json, &config, {.allow_unchecked_cef_path = true});
EXPECT_EQ(ConfigError::kSuccess, error);
EXPECT_TRUE(config.unchecked_cef_path.empty());
}
TEST_F(InstallerConfigTest, ParseUncheckedCefPathAbsentDefault) {
const char* json = R"({
"appid": "12345678-1234-1234-1234-123456789abc",
"vmin": "137.1"
})";
Config config;
ConfigError error =
ParseConfigFromJson(json, &config, {.allow_unchecked_cef_path = true});
EXPECT_EQ(ConfigError::kSuccess, error);
EXPECT_TRUE(config.unchecked_cef_path.empty());
}
TEST_F(InstallerConfigTest, ParseUncheckedCefPathIgnoredByDefault) {
// When allow_unchecked_cef_path is false (default), the field is ignored
const char* json = R"({
"appid": "12345678-1234-1234-1234-123456789abc",
"vmin": "137.1",
"unchecked_cef_path": "C:\\App\\CEF"
})";
Config config;
ConfigError error = ParseConfigFromJson(json, &config);
EXPECT_EQ(ConfigError::kSuccess, error);
EXPECT_TRUE(config.unchecked_cef_path.empty());
}
// ============================================================================
// bundled_cef_path Tests
// ============================================================================
TEST_F(InstallerConfigTest, ParseBundledCefPath) {
const char* json = R"({
"appid": "12345678-1234-1234-1234-123456789abc",
"vmin": "137.1",
"bundled_cef_path": "bundled_cef"
})";
Config config;
ConfigError error =
ParseConfigFromJson(json, &config, {.allow_bundled_cef_path = true});
EXPECT_EQ(ConfigError::kSuccess, error);
EXPECT_EQ("bundled_cef", config.bundled_cef_path);
}
TEST_F(InstallerConfigTest, ParseBundledCefPathAbsolute) {
const char* json = R"({
"appid": "12345678-1234-1234-1234-123456789abc",
"vmin": "137.1",
"bundled_cef_path": "C:\\App\\BundledCEF"
})";
Config config;
ConfigError error =
ParseConfigFromJson(json, &config, {.allow_bundled_cef_path = true});
EXPECT_EQ(ConfigError::kSuccess, error);
EXPECT_EQ("C:\\App\\BundledCEF", config.bundled_cef_path);
}
TEST_F(InstallerConfigTest, ParseBundledCefPathEmptyIgnored) {
const char* json = R"({
"appid": "12345678-1234-1234-1234-123456789abc",
"vmin": "137.1",
"bundled_cef_path": ""
})";
Config config;
ConfigError error =
ParseConfigFromJson(json, &config, {.allow_bundled_cef_path = true});
EXPECT_EQ(ConfigError::kSuccess, error);
EXPECT_TRUE(config.bundled_cef_path.empty());
}
TEST_F(InstallerConfigTest, ParseBundledCefPathIgnoredByDefault) {
const char* json = R"({
"appid": "12345678-1234-1234-1234-123456789abc",
"vmin": "137.1",
"bundled_cef_path": "C:\\App\\BundledCEF"
})";
Config config;
ConfigError error = ParseConfigFromJson(json, &config);
EXPECT_EQ(ConfigError::kSuccess, error);
EXPECT_TRUE(config.bundled_cef_path.empty());
}
// ============================================================================
// ConfigErrorToString Tests
// ============================================================================
TEST_F(InstallerConfigTest, ConfigErrorToString) {
EXPECT_STREQ("Success", ConfigErrorToString(ConfigError::kSuccess));
EXPECT_STREQ("Malformed JSON",
ConfigErrorToString(ConfigError::kJsonParseError));
EXPECT_STREQ("Required field missing",
ConfigErrorToString(ConfigError::kMissingRequiredField));
EXPECT_STREQ("Invalid field value",
ConfigErrorToString(ConfigError::kInvalidFieldValue));
EXPECT_STREQ("Embedded resource not found",
ConfigErrorToString(ConfigError::kResourceNotFound));
}
TEST_F(InstallerConfigTest, ParseInstallPathAllowedForClientResource) {
const std::string vmin =
Version::FromApiVersion(CEF_API_VERSION_LAST).ToString();
const std::string json =
R"({"appid":"12345678-1234-1234-1234-123456789abc","vmin":")" + vmin +
R"(","install_path":"cef\\private"})";
Config config;
EXPECT_EQ(ConfigError::kSuccess,
ParseConfigFromJson(json, &config, {.allow_install_path = true}));
EXPECT_EQ("cef\\private", config.install_path);
}
TEST_F(InstallerConfigTest, ParseInstallPathEmptyAndMissingClearValue) {
const std::string vmin =
Version::FromApiVersion(CEF_API_VERSION_LAST).ToString();
const std::string prefix =
R"({"appid":"12345678-1234-1234-1234-123456789abc","vmin":")" + vmin;
Config config;
ASSERT_EQ(ConfigError::kSuccess,
ParseConfigFromJson(prefix + R"(","install_path":"first"})",
&config, {.allow_install_path = true}));
ASSERT_EQ("first", config.install_path);
EXPECT_EQ(ConfigError::kSuccess,
ParseConfigFromJson(prefix + R"(","install_path":""})", &config,
{.allow_install_path = true}));
EXPECT_TRUE(config.install_path.empty());
config.install_path = "stale";
EXPECT_EQ(ConfigError::kSuccess,
ParseConfigFromJson(prefix + R"("})", &config,
{.allow_install_path = true}));
EXPECT_TRUE(config.install_path.empty());
}
TEST_F(InstallerConfigTest, ParseInstallPathWrongTypeRejected) {
const std::string vmin =
Version::FromApiVersion(CEF_API_VERSION_LAST).ToString();
const std::string json =
R"({"appid":"12345678-1234-1234-1234-123456789abc","vmin":")" + vmin +
R"(","install_path":7})";
Config config;
std::string diagnostic;
EXPECT_EQ(ConfigError::kInvalidFieldValue,
ParseConfigFromJson(json, &config, {.allow_install_path = true},
&diagnostic));
EXPECT_EQ("install_path must be a string", diagnostic);
EXPECT_TRUE(config.install_path.empty());
}
TEST_F(InstallerConfigTest, ParseInstallPathDisallowedAndReuseClearsValue) {
const std::string vmin =
Version::FromApiVersion(CEF_API_VERSION_LAST).ToString();
const std::string json =
R"({"appid":"12345678-1234-1234-1234-123456789abc","vmin":")" + vmin +
R"(","install_path":"ignored"})";
Config config;
ASSERT_EQ(ConfigError::kSuccess,
ParseConfigFromJson(json, &config, {.allow_install_path = true}));
ASSERT_EQ("ignored", config.install_path);
EXPECT_EQ(ConfigError::kSuccess, ParseConfigFromJson(json, &config));
EXPECT_TRUE(config.install_path.empty());
}
// ============================================================================
// ConfigToJson Tests
// ============================================================================
TEST_F(InstallerConfigTest, ConfigToJson_AllFields) {
// Use vmin based on CEF_API_VERSION_LAST so it won't be clamped
Version next_ver = Version::FromApiVersion(CEF_API_VERSION_LAST);
std::string vmin_str = next_ver.ToString();
Config config;
config.appid = "12345678-1234-1234-1234-123456789abc";
config.vmin = vmin_str;
config.vmax = vmin_str + ".99";
config.abi_hash = "abc123def4567890";
config.channel = "beta";
config.cdn_urls = {"https://one.example/", "https://one.example/"};
config.launch_health = LaunchHealthMode::kExplicit;
config.enable_explicit_modes = true;
config.unchecked_cef_path = "C:\\App\\CEF";
config.bundled_cef_path = "C:\\App\\BundledCEF";
config.install_path = "C:\\App\\PrivateStore";
std::string json = ConfigToJson(config);
EXPECT_EQ(std::string::npos, json.find("install_path"));
// Round-trip: parse the JSON back and verify all fields
Config parsed;
EXPECT_EQ(ConfigError::kSuccess,
ParseConfigFromJson(json, &parsed,
{.allow_enable_explicit_modes = true,
.allow_unchecked_cef_path = true,
.allow_bundled_cef_path = true}));
EXPECT_EQ(config.appid, parsed.appid);
EXPECT_EQ(config.vmin, parsed.vmin);
EXPECT_EQ(config.vmax, parsed.vmax);
EXPECT_EQ(config.abi_hash, parsed.abi_hash);
EXPECT_EQ(config.channel, parsed.channel);
EXPECT_EQ(config.cdn_urls, parsed.cdn_urls);
EXPECT_EQ(config.launch_health, parsed.launch_health);
EXPECT_EQ(config.enable_explicit_modes, parsed.enable_explicit_modes);
EXPECT_EQ(config.unchecked_cef_path, parsed.unchecked_cef_path);
EXPECT_EQ(config.bundled_cef_path, parsed.bundled_cef_path);
}
TEST_F(InstallerConfigTest, ConfigToJson_RequiredFieldsOnly) {
// Use vmin based on CEF_API_VERSION_LAST so it won't be clamped
Version next_ver = Version::FromApiVersion(CEF_API_VERSION_LAST);
std::string vmin_str = next_ver.ToString();
Config config;
config.appid = "12345678-1234-1234-1234-123456789abc";
config.vmin = vmin_str;
std::string json = ConfigToJson(config);
Config parsed;
EXPECT_EQ(ConfigError::kSuccess, ParseConfigFromJson(json, &parsed));
EXPECT_EQ(config.appid, parsed.appid);
EXPECT_EQ(config.vmin, parsed.vmin);
EXPECT_TRUE(parsed.vmax.empty());
EXPECT_TRUE(parsed.abi_hash.empty());
EXPECT_TRUE(parsed.channel.empty());
EXPECT_TRUE(parsed.cdn_urls.empty());
EXPECT_EQ(LaunchHealthMode::kOff, parsed.launch_health);
EXPECT_FALSE(parsed.enable_explicit_modes);
EXPECT_TRUE(parsed.unchecked_cef_path.empty());
EXPECT_TRUE(parsed.bundled_cef_path.empty());
}
// ============================================================================
// ReadConfigFromResource Tests
// ============================================================================
TEST_F(InstallerConfigTest, ReadConfigFromResource_NullConfig) {
// Passing nullptr should return an error, not crash
EXPECT_EQ(ConfigError::kMissingRequiredField,
ReadConfigFromResource(GetModuleHandle(nullptr), nullptr));
}
TEST_F(InstallerConfigTest, ReadConfigFromResource_ResourceNotFound) {
Config config;
// Current module has no CEF_INSTALLER_CONFIG resource
EXPECT_EQ(ConfigError::kResourceNotFound,
ReadConfigFromResource(GetModuleHandle(nullptr), &config));
}
TEST_F(InstallerConfigTest, ParseEmptyVmin) {
const char* json = R"({
"appid": "12345678-1234-1234-1234-123456789abc",
"vmin": ""
})";
Config config;
ConfigError error = ParseConfigFromJson(json, &config);
EXPECT_EQ(ConfigError::kMissingRequiredField, error);
}
TEST_F(InstallerConfigTest, ParseInvalidVmin) {
const char* json = R"({
"appid": "12345678-1234-1234-1234-123456789abc",
"vmin": "not-a-version"
})";
Config config;
ConfigError error = ParseConfigFromJson(json, &config);
EXPECT_EQ(ConfigError::kInvalidFieldValue, error);
}
TEST_F(InstallerConfigTest, ParseInvalidVmax) {
const char* json = R"({
"appid": "12345678-1234-1234-1234-123456789abc",
"vmin": "137.1",
"vmax": "not-a-version"
})";
Config config;
ConfigError error = ParseConfigFromJson(json, &config);
EXPECT_EQ(ConfigError::kInvalidFieldValue, error);
}
TEST_F(InstallerConfigTest, ParseNullConfig) {
ConfigError error = ParseConfigFromJson("{}", nullptr);
EXPECT_EQ(ConfigError::kMissingRequiredField, error);
}
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
TEST_F(InstallerConfigTest, ParseCertificateThumbprint) {
Version next_ver = Version::FromApiVersion(CEF_API_VERSION_LAST);
std::string vmin_str = next_ver.ToString();
std::string json =
R"({"appid": "12345678-1234-1234-1234-123456789abc", "vmin": ")" +
vmin_str +
R"(", "certificate_thumbprint": "136B16359FD1209E8255DB538F3F02C8E8D8BB55"})";
Config config;
ConfigError error = ParseConfigFromJson(json, &config);
EXPECT_EQ(ConfigError::kSuccess, error);
EXPECT_EQ("136B16359FD1209E8255DB538F3F02C8E8D8BB55",
config.certificate_thumbprint);
}
TEST_F(InstallerConfigTest, ParseCertificateThumbprintAbsent) {
Version next_ver = Version::FromApiVersion(CEF_API_VERSION_LAST);
std::string vmin_str = next_ver.ToString();
std::string json =
R"({"appid": "12345678-1234-1234-1234-123456789abc", "vmin": ")" +
vmin_str + R"("})";
Config config;
ConfigError error = ParseConfigFromJson(json, &config);
EXPECT_EQ(ConfigError::kSuccess, error);
EXPECT_TRUE(config.certificate_thumbprint.empty());
}
#endif
} // namespace
} // namespace cef_installer
@@ -0,0 +1,318 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_CONSTANTS_H_
#define CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_CONSTANTS_H_
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <string_view>
namespace cef_installer {
// Resource name for embedded JSON configuration (RT_RCDATA)
// Using a string name instead of numeric ID to avoid conflicts with
// application-defined resource IDs.
constexpr const wchar_t* kConfigResourceName = L"CEF_INSTALLER_CONFIG";
// Resource name for compiled-in revocation list (RT_RCDATA)
// Embedded in the bootstrap binary at build time from revoked.json.
constexpr const wchar_t* kRevocationResourceName = L"CEF_REVOCATION_LIST";
// Default CDN base URL for CEF builds
constexpr std::string_view kDefaultCdnBaseUrl =
"https://cef-builds.spotifycdn.com/";
// Hardcoded certificate thumbprint for official CEF signed binaries (SHA-1)
// This is the thumbprint of the code signing certificate used to sign
// all official CEF distributions. Must be updated when certificate is rotated.
// Format: 40 hex characters, uppercase, no separators
constexpr std::string_view kCefCertificateThumbprint =
"3906F72B1D5FA45A6334A8590495FAEB93347E5B";
// Revocation list URL (relative to CDN base)
constexpr std::string_view kRevocationListPath = "revoked.json";
// Release channels
constexpr std::string_view kChannelStable = "stable"; // Default channel
constexpr std::string_view kChannelBeta = "beta";
// Installer database filename
constexpr std::wstring_view kDatabaseFilename = L"installer.json";
// Version metadata filename (per version directory)
constexpr std::wstring_view kVersionMetadataFilename = L"cef_version.json";
// Version index filename (per install directory root)
constexpr std::wstring_view kVersionIndexFilename = L"versions.json";
// CEF library filename
constexpr std::wstring_view kLibcefFilename = L"libcef.dll";
// Release subdirectory within CEF distribution archives.
// CEF distributions place binaries and resources in a Release/ subdirectory.
constexpr std::wstring_view kReleaseSubdirectory = L"Release";
// Catalog filename for signature verification
constexpr std::wstring_view kCatalogFilename = L"catalog.cat";
// Revocation cache filename (disk-cached CDN delta)
constexpr std::wstring_view kRevocationCacheFilename = L"revocation_cache.json";
constexpr std::wstring_view kRevocationBackoffFilename =
L"revocation_refresh_backoff.json";
constexpr int kRevocationCacheValiditySeconds = 60 * 60;
constexpr int kRevocationFailureBackoffSeconds = 15 * 60;
constexpr int64_t kMaxRevocationBackoffFileSize = 4096;
constexpr int kLaunchRevocationConnectTimeoutMs = 3000;
constexpr int kLaunchRevocationReceiveTimeoutMs = 2000;
constexpr int kLaunchRevocationOverallTimeoutMs = 5000;
// Automatic-startup emergency recovery limits. The scanner runs only after
// index-backed and bundled selection misses, and only for roots whose index is
// missing or invalid. The elapsed budget is soft because a synchronous Windows
// filesystem operation cannot be interrupted at the deadline.
constexpr size_t kEmergencyRecoveryMaxRoots = 4;
constexpr size_t kEmergencyRecoveryMaxVersionEntries = 128;
constexpr int kEmergencyRecoveryTimeBudgetMs = 500;
// Installer log filename
constexpr std::wstring_view kLogFilename = L"cef_installer.log";
// Maximum log file size before rotation (1 MB)
constexpr uint64_t kMaxLogFileSize = 1 * 1024 * 1024;
// Number of rotated log files to keep
constexpr int kMaxRotatedLogFiles = 3;
// Mutex name prefix for installer lock
// Full name: Global\CEF_Installer_<hash>
constexpr std::string_view kMutexNamePrefix = "Global\\CEF_Installer_";
// Default lock acquisition timeout (30 seconds)
constexpr uint32_t kDefaultLockTimeoutMs = 30000;
// Default download retry settings
constexpr int kDefaultMaxRetries = 3;
constexpr uint32_t kDefaultRetryDelayMs = 1000;
// Manifest cache validity duration (1 hour)
constexpr int kManifestCacheValiditySeconds = 3600;
// Archive cache expiry duration (7 days). Cached archives are kept across
// retries and only deleted after successful installation. This expiry catches
// orphaned archives from failed installs that were never retried.
constexpr int kArchiveCacheValiditySeconds = 7 * 24 * 60 * 60;
// CEF subdirectory name within standard locations (e.g., %ProgramFiles%\CEF)
constexpr std::wstring_view kCefSubdirectory = L"CEF";
// Versions subdirectory within the install directory
constexpr std::wstring_view kVersionsSubdirectory = L"Versions";
// Staging subdirectory for downloads and extraction.
// Placed under the install directory to guarantee same-volume atomic renames
// into Versions/<version>/<platform>.
constexpr std::wstring_view kStagingSubdirectory = L".staging";
// Pending deletion directory
constexpr std::wstring_view kTrashSubdirectory = L".trash";
// ============================================================================
// Security: Field Length Limits
// ============================================================================
// Maximum lengths for string fields to prevent memory exhaustion from
// malformed/malicious data. These limits are enforced when parsing JSON
// from untrusted sources (manifests, database, metadata files).
constexpr size_t kMaxVersionLength = 64; // Version strings (e.g., "137.3.5")
constexpr size_t kMaxUuidLength = 256; // Application UUIDs
constexpr size_t kMaxAbiHashLength = 256; // ABI compatibility hashes
constexpr size_t kMaxFilenameLength = 256; // Archive filenames
constexpr size_t kMaxSha1Length = 40; // SHA1 hex strings
constexpr size_t kMaxSha256Length = 64; // SHA256 hex strings
constexpr size_t kMaxTimestampLength = 64; // ISO 8601 timestamps
constexpr size_t kMaxVersionFullLength = 256; // Full version strings
constexpr size_t kMaxReasonLength = 1024; // Revocation reason strings
// Maximum size for CDN revocation list downloads (1 MB).
// Also used to bound the on-disk revocation cache file size (2x multiplier
// to accommodate CRC32 footer overhead and merges across sessions).
constexpr uint64_t kMaxRevocationDownloadSize = 1 * 1024 * 1024;
constexpr int64_t kMaxRevocationCacheFileSize =
static_cast<int64_t>(kMaxRevocationDownloadSize) * 2;
constexpr size_t kMaxPlatformLength =
32; // Platform strings (e.g., "windows64")
// Maximum number of entries in a revocation list. The 2 MB file size cap
// (kMaxRevocationCacheFileSize) naturally limits entries to ~10K, but an
// explicit limit provides a clearer guarantee against CPU exhaustion during
// parsing and merge operations.
constexpr size_t kMaxRevocationEntryCount = 10000;
// ============================================================================
// Process Exit Codes
// ============================================================================
// Used when running in standalone installer mode (bootstrap.exe /cef-update,
// etc.) Exit code 0 indicates success; non-zero indicates specific error
// categories.
//
// These codes use a base offset of 100 to avoid conflicts with cef_resultcode_t
// values from cef_types.h (Chrome codes: 0-40+, sandbox codes: 7006+).
// The 100-199 range is reserved for installer-specific errors.
constexpr int kInstallerExitCodeBase = 100;
// The requested operation completed logically. Physical cleanup may still be
// deferred and reported as a warning.
constexpr int kExitCodeSuccess = 0;
// Required installer configuration was missing or invalid.
constexpr int kExitCodeConfigError = kInstallerExitCodeBase; // 100
// A required download or other network operation failed.
constexpr int kExitCodeNetworkError = kInstallerExitCodeBase + 1; // 101
// Catalog signature or catalog content verification failed.
constexpr int kExitCodeSignatureError = kInstallerExitCodeBase + 2; // 102
// No non-revoked CEF version satisfied the requested constraints.
constexpr int kExitCodeNoMatchingVersion = kInstallerExitCodeBase + 3; // 103
// The downloaded archive could not be extracted safely.
constexpr int kExitCodeExtractionError = kInstallerExitCodeBase + 4; // 104
// Installer setup or publication failure, including indeterminate physical
// containment or a required uninstall relaunch that could not be prepared or
// started.
constexpr int kExitCodeInstallError = kInstallerExitCodeBase + 5; // 105
// The registration database could not be read, updated, or published.
constexpr int kExitCodeDatabaseError = kInstallerExitCodeBase + 6; // 106
// The writer lock could not be acquired before the configured timeout.
constexpr int kExitCodeLockTimeout = kInstallerExitCodeBase + 7; // 107
// The operation was cancelled before its logical commit.
constexpr int kExitCodeCancelled = kInstallerExitCodeBase + 8; // 108
// Uninstall work is pending in a successfully created child process. Returned
// only for explicit background mode or self-removal from the writable target.
constexpr int kExitCodeRelaunched = kInstallerExitCodeBase + 9; // 109
// No active launch-health sentinel was found for launch-success confirmation.
constexpr int kExitCodeNoSentinel = kInstallerExitCodeBase + 10; // 110
// The launch-health sentinel could not be read, validated, or confirmed.
constexpr int kExitCodeSentinelReadError = kInstallerExitCodeBase + 11; // 111
// The launch-health sentinel belongs to a different process instance.
constexpr int kExitCodeSentinelOwnerMismatch =
kInstallerExitCodeBase + 12; // 112
// Valid enterprise policy prohibited a required mutation or download.
constexpr int kExitCodePolicyDenied = kInstallerExitCodeBase + 13; // 113
// The checked version index could not be read, published, or revalidated.
constexpr int kExitCodeIndexError = kInstallerExitCodeBase + 14; // 114
// Writer-locked staging, index, or orphan recovery failed.
constexpr int kExitCodeRecoveryError = kInstallerExitCodeBase + 15; // 115
// Verified staging could not replace a proven-invalid destination.
constexpr int kExitCodeRepairError = kInstallerExitCodeBase + 16; // 116
// An invalid destination could not be moved safely to quarantine or trash.
constexpr int kExitCodeQuarantineError = kInstallerExitCodeBase + 17; // 117
// Final validation found that the retention removal scope had changed.
constexpr int kExitCodeRetentionSnapshotChanged =
kInstallerExitCodeBase + 18; // 118
// Enterprise policy was unreadable, malformed, or internally conflicting.
constexpr int kExitCodePolicyError = kInstallerExitCodeBase + 19; // 119
// An unexpected or otherwise unclassified internal error occurred.
constexpr int kExitCodeUnknownError = kInstallerExitCodeBase + 99; // 199
// ============================================================================
// WM_COPYDATA Progress Notifications
// ============================================================================
// Used for progress communication between the installer and a parent window.
// COPYDATASTRUCT.dwData identifier for CEF installer progress messages.
// 'CEFI' in little-endian: 0x49464543
// Type is uintptr_t to match ULONG_PTR without requiring <windows.h>.
constexpr uintptr_t kWmCopyDataInstallerProgress = 0x43454649;
// Separate 'CEFR' channel for uninstall lifecycle handoff and terminal result
// envelopes. Existing progress receivers may ignore this additive identity.
constexpr uintptr_t kWmCopyDataInstallerLifecycle = 0x43454652;
constexpr int kInstallerLifecycleProtocolVersion = 1;
constexpr size_t kInstallerLifecycleMaxPayloadSize = 32 * 1024;
constexpr size_t kInstallerLifecycleMaxErrorMessageSize = 4096;
constexpr size_t kInstallerLifecycleMaxWarningSize = 1024;
constexpr size_t kInstallerLifecycleMaxWarnings = 32;
constexpr uint32_t kInstallerLifecycleSendTimeoutMs = 500;
// Return this value from a WM_COPYDATA handler to request cancellation.
// Standard WM_COPYDATA return values (TRUE=handled, FALSE=not handled) are
// not treated as cancellation — only this specific sentinel value cancels.
// Type is intptr_t to match LRESULT without requiring <windows.h>.
constexpr intptr_t kWmCopyDataResultCancel = 2;
// ============================================================================
// Progress Steps
// ============================================================================
// Step indices for installer progress. Values are used as array indices.
enum Step {
kStepInit = 0,
kStepLock,
kStepVersionCheck,
kStepCdnResolve,
kStepDownload,
kStepExtract,
kStepSignatureVerify,
kStepInstall,
kStepCommitting,
kStepCleanup,
kNumSteps,
};
// Display strings for each step (English fallback).
inline constexpr const char* kStepDisplayStrings[] = {
"Initializing...", // kStepInit
"Initializing...", // kStepLock
"Checking versions...", // kStepVersionCheck
"Checking versions...", // kStepCdnResolve
"Downloading...", // kStepDownload
"Extracting...", // kStepExtract
"Verifying signatures...", // kStepSignatureVerify
"Installing...", // kStepInstall
"Committing...", // kStepCommitting
"Cleaning up...", // kStepCleanup
};
static_assert(std::size(kStepDisplayStrings) == kNumSteps,
"kStepDisplayStrings size mismatch");
// Returns the display string for a step (e.g., "Downloading...").
inline const char* StepDisplayString(Step step) {
if (step < 0 || step >= kNumSteps) {
return "Unknown...";
}
return kStepDisplayStrings[step];
}
// ============================================================================
// Installer Commands
// ============================================================================
// Commands that can be executed by the installer.
// Directory name for launch state files under the install directory.
// Files in this directory survive version pruning so crash history is
// preserved.
constexpr std::wstring_view kLaunchStateDirName = L".launch";
constexpr int64_t kMaxLaunchStateFileSize = 4096;
constexpr uint64_t kLivenessRefreshIntervalFileTimeTicks =
30ULL * 24 * 60 * 60 * 10000000;
constexpr uint64_t kLaunchStateGcMaxAge = 90ULL * 24 * 60 * 60 * 10000000;
// Maximum consecutive launch failures before a version is disqualified
constexpr int kMaxConsecutiveFailures = 3;
enum class Command {
kInstall, // Install CEF for an application
kUpdate, // Update CEF to latest compatible version
kUninstall, // Unregister application and prune unused versions
kQuery, // Query installed versions (no modifications)
kPrune, // Prune unused versions only (no version resolution)
kRetentionDryRun, // Report explicit registration-retention effects
kRetentionApply, // Apply recomputed registration retention
kLaunchSuccess, // Confirm launch health — version is working
};
} // namespace cef_installer
#endif // CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_CONSTANTS_H_
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,629 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_CONTROLLER_H_
#define CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_CONTROLLER_H_
#include <windows.h>
#include <array>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <vector>
#include "base/files/file_path.h"
#include "base/functional/callback.h"
#include "cef/libcef_dll/bootstrap/installer/installer_cdn_manifest.h"
#include "cef/libcef_dll/bootstrap/installer/installer_config.h"
#include "cef/libcef_dll/bootstrap/installer/installer_constants.h"
#include "cef/libcef_dll/bootstrap/installer/installer_database.h"
#include "cef/libcef_dll/bootstrap/installer/installer_file_ops.h"
#include "cef/libcef_dll/bootstrap/installer/installer_lock.h"
#include "cef/libcef_dll/bootstrap/installer/installer_logger.h"
#include "cef/libcef_dll/bootstrap/installer/installer_paths.h"
#include "cef/libcef_dll/bootstrap/installer/installer_policy.h"
#include "cef/libcef_dll/bootstrap/installer/installer_retention.h"
#include "cef/libcef_dll/bootstrap/installer/installer_revocation.h"
#include "cef/libcef_dll/bootstrap/installer/installer_version_metadata.h"
namespace cef_installer {
enum class ArchiveError;
enum class SignatureError;
enum class ExecutionContext {
kExplicitCommand,
kAutomaticStartup,
};
enum class UninstallExecution {
kInProcess,
kRelaunch,
kReject,
};
// Process-local uninstall preparation shared by bootstrap preflight and the
// direct controller run. This is never serialized or exposed by RunInstaller.
struct PreparedUninstall {
Command command = Command::kUninstall;
UninstallExecution execution = UninstallExecution::kReject;
std::string config_binding;
std::string install_path;
PolicyLoadResult policy_result;
DirectoryResolutionContext resolution_context;
std::optional<InstallDirectories> directories;
};
// Extended configuration settings
//
// SECURITY / TRUST MODEL:
// =======================
// ExtendedConfig allows overriding security-sensitive settings like CDN URL
// and certificate thumbprint. This is INTENTIONAL and assumes the config JSON
// comes from a TRUSTED source (the client DLL embedding CEF).
//
// The trust model is:
// - Client application is trusted (it's running the installer)
// - Config JSON passed to RunInstaller is trusted (from client DLL)
// - CDN responses are verified via certificate pinning
// - Downloaded binaries are verified via code signature
//
// The override capabilities exist for:
// - Testing with mock CDN servers (cdn_urls)
// - Supporting alternative signing certificates (certificate_thumbprint)
// - Custom deployment scenarios (install_path, bundled_cef_path)
//
// If config JSON could come from untrusted sources (e.g., user input, network),
// the caller MUST validate/sanitize it before passing to RunInstaller.
//
// HOW EXTENDED CONFIG IS POPULATED:
// =================================
// ExtendedConfig is populated through multiple sources with the following
// precedence:
//
// 1. **Programmatic (Option C)**: When client DLL calls RunInstaller() with a
// JSON config string, extended settings can be included in the JSON:
// {
// "appid": "...", "vmin": "...", "vmax": "...", "abi_hash": "...",
// "cdn_urls": ["https://custom.cdn.com/"],
// "bundled_cef_path": "C:\\App\\CEF"
// }
// The Controller parses both Config and ExtendedConfig from this JSON.
//
// 2. **Command-line flags**: When running in standalone mode, flags like
// /cef-forcecheck, /cef-background, /cef-headless, /cef-parent are parsed
// into ExtendedConfig by ParseExtendedConfig() in bootstrap_win.cc.
//
// 3. **Defaults**: All fields have sensible defaults (see struct definition).
// If not specified, defaults are used.
//
// The client DLL resource may additionally provide bundled_cef_path and
// install_path. install_path is resolved against that DLL and copied here only
// when an operation-specific value has not already been supplied. Bootstrap
// resources cannot provide install_path.
//
struct ExtendedConfig {
// Ordered operation-specific CDN base URLs. Empty means the selected
// application config or hardcoded default applies. Public JSON parsing
// requires one through three HTTPS URLs; non-official tests may inject HTTP
// values directly after parsing.
std::vector<std::string> cdn_urls;
// Custom CEF installation path (default: use ResolveInstallDirectories())
// If empty, uses the standard search order:
// registry -> %ProgramFiles% -> %LocalAppData%
//
// A custom path is authoritative and exclusive. A safe writable path is the
// sole read/write store. A safe existing read-only path is the sole readable
// store for query and automatic selection. Invalid, unsafe, inaccessible, or
// missing read-only paths fail without default-search fallback.
//
// Security measures applied to custom paths:
// - Reparse points (symlinks/junctions) are rejected
std::string install_path;
// Path to bundled CEF for full installers (default: none)
// If set and no newer/compatible version is installed, use this bundled
// version. A newer version may still be installed in background and used
// after relaunch.
//
// Bundled CEF handling:
// 1. The bundled CEF must be pre-extracted (not a .tar.xz archive)
// 2. The bundled directory must contain cef_version.json with
// version/abi_hash
// 3. catalog.cat must be a regular non-reparse file, but its signature and
// members are not verified at runtime
// 4. If bundled version is compatible AND no better installed version
// exists, the bundled version will be loaded in-place
// 5. If a newer compatible version is already installed, bundled CEF is
// ignored
//
// This enables offline/air-gapped installation while still preferring
// newer shared versions when available.
std::string bundled_cef_path;
// Expected certificate thumbprint for CEF binaries.
// Default: kCefCertificateThumbprint (hardcoded official CEF certificate)
//
// SECURITY NOTE: This is the primary security control. Overriding this
// allows binaries signed by a different certificate to be installed.
// Only override for testing or when using legitimately re-signed binaries.
// An attacker who can control this value can bypass signature verification.
std::string certificate_thumbprint = std::string(kCefCertificateThumbprint);
// Force version check even if recently checked
bool force_check = false;
// Show progress UI (default: true)
bool show_progress_ui = true;
// Internal command-line operation context. Set only by /cef-background;
// headless UI suppression alone must not lower extraction impact.
bool background_mode = false;
// Parent window handle for progress UI
// If non-zero and show_progress_ui is true, the progress dialog is positioned
// relative to this window (centered and topmost, but not directly parented
// for security reasons). Also used for WM_COPYDATA progress notifications
// when the caller wants to display custom UI.
HWND parent_window = nullptr;
// Path to a local directory containing CDN-structured files.
// When set, the installer reads manifest JSON, .tar.xz archives, and
// .sha256 files from this directory instead of downloading from CDN.
// The directory must contain the same files that would be available on CDN
// (e.g., {milestone}_{platform}.json, archive .tar.xz, .sha256).
// All usual validation (hash verification, signature checking, version
// filtering) is still performed.
std::string local_download_path;
// Minimum log level for file logging. Messages below this level are
// discarded. Configurable via /cef-log-level=<level>.
LogLevel log_level = LogLevel::kWarning;
// Lock acquisition timeout in milliseconds. 0 = use default
// (kDefaultLockTimeoutMs). Used by post-exit pruning to specify a short
// timeout so it doesn't block process exit.
uint32_t lock_timeout_ms = 0;
// WinHTTP receive-response timeout for archive downloads (milliseconds).
// 0 = use the default (60 s). Shorter values let tests avoid blocking for
// the full default when the server is unresponsive.
int download_timeout_ms = 0;
// Operation-specific registration-retention threshold. This is accepted
// only by explicit retention commands and is never persisted as app config.
int retention_max_age_days = kDefaultRetentionMaxAgeDays;
};
// Applies the test-only certificate thumbprint from |config| to |extended| and
// enables the corresponding signature-verification mode when needed. This is
// a no-op in official release builds.
void ApplyConfigThumbprintOverride(const Config& config,
ExtendedConfig* extended);
struct EmergencyRecoveryScanLimits {
size_t max_roots = kEmergencyRecoveryMaxRoots;
size_t max_version_entries = kEmergencyRecoveryMaxVersionEntries;
base::TimeDelta time_budget =
base::Milliseconds(kEmergencyRecoveryTimeBudgetMs);
};
// Overrides automatic-startup emergency recovery bounds for deterministic
// tests. Passing nullopt restores production limits.
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
void SetEmergencyRecoveryScanLimitsForTesting(
std::optional<EmergencyRecoveryScanLimits> limits);
#endif
// Deterministic auxiliary cleanup failure for retention transaction tests.
void SetRetentionEvidenceDeleteFailureForTesting(bool fail);
void SetRetentionPostIndexFailureForTesting(bool fail);
void SetRetentionPendingRestoreFailureForTesting(bool fail);
void SetRetentionPostValidationEvidenceChangeForTesting(bool change);
void SetLaunchStateGcPreDeleteHookForTesting(base::RepeatingClosure callback);
// Logical completion state for an installer operation. Current producers use
// kCommitted or kFailed. kCleanupDeferred is reserved for command-specific
// cleanup semantics; that outcome is successful.
enum class Outcome {
kCommitted,
kCleanupDeferred,
kFailed,
};
// Result of an installer operation.
struct Result {
bool success = false;
Outcome outcome = Outcome::kFailed;
// Error information (if !success)
int error_code = kExitCodeSuccess; // One of kExitCode* constants
// Unlocalized developer/support diagnostic. Never use for localized UI.
std::string error_message;
// Non-fatal work left for a later mutating pass. Present only with a
// successful kCleanupDeferred outcome.
std::vector<std::string> warnings;
// Internal per-version cleanup effects. Not serialized independently;
// retention copies these into each version report.
std::set<VersionKey> deferred_version_cleanup;
// Present for explicit retention operations.
std::optional<RetentionPlan> retention_plan;
int retention_max_age_days = 0;
bool registrations_committed = false;
bool versions_pruned = false;
bool retry_required = false;
// On successful install: path to libcef.dll
base::FilePath libcef_path;
// Version that was installed/is being used
std::string installed_version;
// Full version string from metadata (e.g.,
// "150.0.1+gabc1234+chromium-150.0.7871.4")
std::string version_full;
// Whether the resolved libcef.dll came from a bundled CEF directory
// (as opposed to a CDN-downloaded installed version). When true, the
// libcef.dll may be signed with the client application's certificate
// rather than the CEF certificate (kCefCertificateThumbprint).
bool is_bundled = false;
// Whether the selected version is a rollback (a different version was
// selected because the newest version was disqualified by launch health).
bool is_rollback = false;
// Path to the launch state file for the bootstrap to write sentinel
// and update after RunWinMain. Empty if launch state is not active.
base::FilePath launch_state_path;
// Consecutive failure count for the selected version. The bootstrap writes
// this value into the sentinel before RunWinMain.
int launch_consecutive_failures = 0;
// Version and platform for the sentinel file. Avoids re-parsing the
// launch_state_path filename in the bootstrap.
std::string launch_version;
std::string launch_platform;
// Paths to .launch_* files from older versions. The bootstrap deletes
// these on successful exit (version confirmed).
std::vector<base::FilePath> launch_cleanup_paths;
// Version-less retention liveness record written before RunWinMain for
// every launch-health mode.
base::FilePath liveness_path;
// Internal use-time lease for an installed shared-store distribution.
// Not serialized in the public Result JSON.
std::shared_ptr<VersionLease> version_lease;
// Convert to JSON string for RunInstaller return value
std::string ToJson() const;
// Parse from JSON string
static std::optional<Result> FromJson(const std::string& json);
// Factory methods for common results
static Result Success(const base::FilePath& libcef_path,
const std::string& version,
const std::string& version_full = "",
bool is_bundled = false);
static Result Error(int code, const std::string& message);
};
// Progress callback for UI updates.
// Parameters: step, bytes_done, bytes_total.
// Return false to cancel the operation.
using ProgressCallback =
base::RepeatingCallback<bool(Step, uint64_t, uint64_t)>;
// Main installer controller class
class Controller {
public:
Controller();
~Controller();
Controller(const Controller&) = delete;
Controller& operator=(const Controller&) = delete;
// Execute an installer command.
// This is the main entry point implementing the RunInstaller API.
//
// Query and automatic-startup resolution use lock-free read paths.
// Mutating commands acquire the singleton writer mutex, reconcile state,
// update the database, perform any download/install work, and prune.
//
// Parameters:
// - command: The operation to perform
// - app_config: Application configuration (appid, vmin, vmax, abi_hash).
// Must be pre-validated via ParseConfigFromJson() or
// equivalent; only a lightweight empty-field check is
// performed here.
// - extended_config: Optional extended settings
// - progress: Optional callback for progress updates
//
// Returns: Result with success/failure info and libcef.dll path on success
Result Run(Command command,
const Config& app_config,
const ExtendedConfig& extended_config = {},
ProgressCallback progress = {},
ExecutionContext context = ExecutionContext::kExplicitCommand);
// Internal bootstrap entry point for an in-process explicit uninstall. The
// prepared policy and directory snapshot must authorize direct execution and
// match the supplied command configuration. Mismatches fail with
// configuration error before mutation.
Result RunPreparedUninstall(const Config& app_config,
const ExtendedConfig& extended_config,
const PreparedUninstall& prepared,
ProgressCallback progress = {});
// Convenience overload that parses config from JSON string
// (for RunInstaller export function)
Result Run(Command command,
const std::string& config_json,
ProgressCallback progress = {});
// Read version indexes from multiple directories and merge results.
// Deduplicates by (version, platform), keeping the entry from the first
// (higher-priority) directory. When |allow_scan_fallback| is true, a
// missing or corrupt index falls back to directory scanning.
static std::vector<InstalledVersion> ReadMultipleVersionIndexes(
const std::vector<base::FilePath>& directories,
bool allow_scan_fallback = true,
VersionIndexReadMode read_mode = VersionIndexReadMode::kDoomCorrupt);
private:
Result RunImpl(Command command,
const Config& app_config,
const ExtendedConfig& extended_config,
ProgressCallback progress,
ExecutionContext context,
const PreparedUninstall* prepared_uninstall);
// Individual execution steps
Result AcquireLock(const base::FilePath& install_dir,
uint32_t timeout_ms = 0);
Result ResolveQuery(const Config& config,
const ExtendedConfig& extended,
const base::FilePath& install_dir,
const std::vector<base::FilePath>& read_dirs);
std::optional<Result> ResolveStartupOffline(
const Config& config,
const ExtendedConfig& extended,
const base::FilePath& install_dir,
const std::vector<base::FilePath>& read_dirs,
const std::vector<RevokedVersionRange>& revoked,
bool allow_last_resort = false);
void TryRegisterStartup(const Config& config,
const base::FilePath& install_dir);
Result UpdateDatabase(Command command,
const Config& config,
const base::FilePath& install_dir);
Result PublishRegistration(const Config& config,
const base::FilePath& install_dir);
Result ReconcileInstallState(const base::FilePath& install_dir);
Result ComputeAndInstall(const Config& config,
const ExtendedConfig& extended,
const base::FilePath& install_dir,
const std::vector<base::FilePath>& read_dirs,
Command command,
ProgressCallback progress);
// Prune binaries and repair launch debris. Ordinary pruning age-gates valid
// orphaned/below-vmin launch records without allowing them to protect
// versions; explicit retention apply does not run that age GC.
Result PruneUnusedVersions(
const base::FilePath& install_dir,
const Config& current_config,
std::vector<InstalledVersion> installed = {},
const std::vector<RevokedVersionRange>& revoked = {},
bool retention_apply = false,
const std::set<VersionKey>* retention_scope = nullptr,
base::OnceCallback<bool()> logical_commit_complete = {});
Result RunRetention(Command command,
DirectoryRole role,
const base::FilePath& install_dir,
const std::vector<base::FilePath>& read_dirs,
const ExtendedConfig& extended_config,
ProgressCallback progress);
void ReleaseLock();
// CDN operations
std::optional<CdnBuildEntry> QueryCdnForVersion(
const Config& config,
const ExtendedConfig& extended,
const base::FilePath& install_dir,
const std::set<Version>& skip_versions = {},
std::vector<CdnBuildEntry>* validated_entries = nullptr);
Result DownloadAndInstall(const CdnBuildEntry& entry,
const ExtendedConfig& extended,
const base::FilePath& install_dir,
ProgressCallback progress,
bool* try_next_candidate = nullptr);
// Check if bundled CEF (from full installers) is suitable for use.
// Reads valid cef_version.json from bundled_path and returns the candidate.
// Platform, range, ABI, revocation, and disqualification are applied by the
// shared side-effect-free selector.
// catalog.cat must exist as a regular non-reparse file, but its signature and
// members are not verified — hashing ~370 MB on every startup is too costly.
// Bundled integrity is the app developer's responsibility (their package).
// Returns nullopt when the layout or metadata is invalid.
std::optional<InstalledVersion> CheckBundledCef(
const Config& config,
const ExtendedConfig& extended);
// Construct a validated Result::Success for a libcef.dll path. Calls
// GetLibcefPath and validates that every component between |trusted_root|
// and the resulting path is free of reparse points. Returns an error
// Result if validation fails.
Result MakeValidatedLibcefResult(const base::FilePath& trusted_root,
const base::FilePath& version_dir,
const std::string& version_str,
const std::string& version_full = "",
bool is_bundled = false);
// Internal state
std::unique_ptr<SingletonLock> lock_;
std::unique_ptr<Database> database_;
bool cancelled_ = false;
bool version_published_ = false;
base::FilePath pending_archive_cleanup_path_;
std::vector<RevokedVersionRange> effective_revocations_;
EnterprisePolicy enterprise_policy_;
EffectiveDownloadSource effective_download_source_;
};
// Get the process exit code for a result
int ResultToExitCode(const Result& result);
// Convert an outcome to its normalized JSON/logging representation.
const char* OutcomeToString(Outcome outcome);
// Calculate overall progress percentage for the installer.
// Maps step/byte progress to a 0-100% range where each step gets a portion:
// Steps 0-1 (Init/Lock): 0-5%
// Steps 2-3 (VersionCheck/CdnResolve): 5-15%
// Step 4 (Download): 15-55%
// Step 5 (Extract): 55-80%
// Step 6 (SignatureVerify): 80-90%
// Step 7 (Install): 90-95%
// Step 8 (Committing): 95% (retention apply only)
// Step 9 (Cleanup): 95-100%
// If bytes_total > 0, interpolates within the step based on byte progress.
int CalculateOverallProgress(Step step,
uint64_t bytes_done,
uint64_t bytes_total);
// Send progress notification to parent window via WM_COPYDATA.
// Rate-limited to avoid flooding the receiver during rapid progress updates.
// Uses SendMessageTimeout with SMTO_ABORTIFHUNG to prevent hanging.
// Returns true to continue, false if the parent requested cancellation
// (by returning kWmCopyDataResultCancel from its WM_COPYDATA handler).
// Timeouts and errors are not treated as cancellation.
bool SendProgressToParent(HWND parent,
Step step,
uint64_t bytes_done,
uint64_t bytes_total);
// ============================================================================
// Internal helpers exposed for testing. Not part of the public API.
// ============================================================================
namespace internal {
// Typed candidate-fallback classification shared by production control flow
// and the retry-matrix unit tests.
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
bool IsCandidateRetryableArchiveErrorForTesting(ArchiveError error);
bool IsCandidateRetryableSignatureErrorForTesting(SignatureError error);
bool IsCandidateRetryableMetadataErrorForTesting(MetadataError error);
#endif
// Parse ExtendedConfig from JSON string.
// Used when parsing the combined config JSON in RunInstaller.
bool ParseExtendedConfigFromJson(const std::string& json,
ExtendedConfig* config,
std::string* diagnostic = nullptr);
// Parse both Config and ExtendedConfig from a combined JSON string.
bool ParseCombinedConfig(const std::string& json,
Config* config,
ExtendedConfig* extended,
std::string* diagnostic = nullptr);
// Parse Command from string (case-insensitive)
std::optional<Command> ParseCommand(const std::string& str);
// Handle the kLaunchSuccess command: confirm the active CEF version is
// healthy by writing running=false, consecutive_failures=0 to the sentinel
// file.
//
// This is a lightweight path — it does NOT go through Controller::Run, the
// installer lock, the database, or progress UI. It reads the sentinel,
// verifies PID ownership, and writes the confirmation directly.
//
// Returns a Result with:
// - success=true (kExitCodeSuccess) on confirmation, already-confirmed
// (idempotent), and writes back preserving all other fields.
// - kExitCodeNoSentinel if |launch_state_path| is empty.
// - kExitCodeSentinelReadError if the sentinel is missing or corrupt.
// - kExitCodeSentinelOwnerMismatch if the sentinel is owned by another
// process (PID/start-time mismatch).
//
// The no-arg overload resolves the path from the process-global active launch
// state path (set by the bootstrap before RunWinMain). The path-taking
// overload lets tests exercise the logic without mutating process-global
// state.
Result HandleLaunchSuccess();
Result HandleLaunchSuccess(const base::FilePath& launch_state_path);
// Convert between exit code ints and JSON string representations.
const char* ExitCodeToString(int code);
int StringToExitCode(const std::string& str);
std::optional<Outcome> StringToOutcome(const std::string& str);
// Returns the canonical name for a step (e.g., "downloading").
// Used in WM_COPYDATA JSON progress notifications.
const char* StepCanonicalName(Step step);
// Build JSON string for WM_COPYDATA progress notification.
std::string BuildProgressJson(Step step,
uint64_t bytes_done,
uint64_t bytes_total);
// Reset rate limiting state for SendProgressToParent (for testing).
void ResetProgressNotificationState();
// Enable testing mode for RunInstaller.
// When enabled, allows HTTP URLs and suppresses error dialogs.
// Call with true in test SetUp, false in TearDown.
void SetTestingMode(bool enabled);
} // namespace internal
// ============================================================================
// DLL Export Function
// ============================================================================
// Exported function for client DLL to call (Option C: Programmatic
// Configuration). This is the DLL export that wraps Controller.
//
// Parameters:
// - command: "install", "update", "uninstall", or "query"
// - config_json: JSON configuration string (or empty for default)
//
// Returns: JSON result with boolean success and an outcome of "committed",
// "cleanup_deferred", or "failed". Failed results contain numeric
// error_code, symbolic error_name, and error_message. Successful
// results omit error fields and may include libcef_path,
// installed_version, version_full, and is_bundled.
// The returned pointer is valid until the next call to RunInstaller
// on the same thread or until that thread exits. Copy the JSON before
// either event if it must be retained.
//
// Thread safety: Uses thread-local storage for the returned string, so
// concurrent calls from different threads are safe. The installer itself
// serializes operations via a global lock.
//
// Example usage from client DLL:
// const char* result = RunInstaller("install",
// R"({"appid":"...", "vmin":"137.1"})");
// auto parsed = Result::FromJson(result);
// if (parsed && parsed->success &&
// parsed->outcome != Outcome::kFailed)
// LoadLibrary(parsed->libcef_path);
extern "C" __declspec(dllexport) const char* RunInstaller(
const char* command,
const char* config_json);
} // namespace cef_installer
#endif // CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_CONTROLLER_H_
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,20 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cef/libcef_dll/bootstrap/installer/installer_crc.h"
#include "third_party/lzma_sdk/src/C/7zCrc.h"
#include "third_party/lzma_sdk/src/C/XzCrc64.h"
namespace cef_installer {
void EnsureCrcInitialized() {
[[maybe_unused]] static bool initialized = [] {
CrcGenerateTable();
Crc64GenerateTable();
return true;
}();
}
} // namespace cef_installer
@@ -0,0 +1,17 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_CRC_H_
#define CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_CRC_H_
namespace cef_installer {
// One-time initialization of CRC32 and CRC64 lookup tables (lzma_sdk).
// Safe to call multiple times; subsequent calls are no-ops.
// Must be called before CrcCalc() or any XZ decompression.
void EnsureCrcInitialized();
} // namespace cef_installer
#endif // CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_CRC_H_
@@ -0,0 +1,343 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cef/libcef_dll/bootstrap/installer/installer_database.h"
#include <time.h>
#include <algorithm>
#include "base/files/file_util.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/strings/string_number_conversions.h"
#include "cef/libcef_dll/bootstrap/installer/installer_constants.h"
#include "cef/libcef_dll/bootstrap/installer/installer_e2e_config.h"
#include "cef/libcef_dll/bootstrap/installer/installer_file_integrity.h"
#include "cef/libcef_dll/bootstrap/installer/installer_file_ops.h"
namespace cef_installer {
namespace {
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
bool g_fail_database_save = false;
#endif
bool ShouldFailDatabaseSave() {
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
if (g_fail_database_save) {
return true;
}
return internal::GetInstallerE2EConfig().database_save_failure;
#else
return false;
#endif
}
// JSON field names
constexpr char kSchemaVersionField[] = "schema_version";
constexpr char kAppsField[] = "apps";
constexpr char kUuidField[] = "uuid";
constexpr char kPlatformField[] = "platform";
constexpr char kVminField[] = "vmin";
constexpr char kVmaxField[] = "vmax";
constexpr char kAbiHashField[] = "abi_hash";
constexpr char kPruningSuspendedUntilField[] = "pruning_suspended_until";
} // namespace
void SetDatabaseSaveFailureForTesting(bool fail) {
#if defined(OFFICIAL_BUILD) && defined(NDEBUG)
(void)fail;
#else
g_fail_database_save = fail;
#endif
}
bool AppEntry::operator==(const AppEntry& other) const {
return uuid == other.uuid && platform == other.platform &&
vmin == other.vmin && vmax == other.vmax && abi_hash == other.abi_hash;
}
Database::Database() = default;
Database::~Database() = default;
DatabaseError Database::Load(const base::FilePath& path,
IntegrityMismatchAction mismatch_action) {
// Reset to defaults
schema_version_ = kCurrentSchemaVersion;
apps_.clear();
pruning_suspended_until_ = 0;
std::string json_content;
IntegrityResult ir =
ReadFileWithIntegrity(path, &json_content, mismatch_action);
switch (ir) {
case IntegrityResult::kFileNotFound:
// File doesn't exist - start with empty database
return DatabaseError::kSuccess;
case IntegrityResult::kReadError:
return DatabaseError::kFileReadError;
case IntegrityResult::kIntegrityMismatch:
// Corrupted file was already deleted by ReadFileWithIntegrity.
// Return empty database — caller should call SuspendPruning().
return DatabaseError::kIntegrityMismatch;
case IntegrityResult::kSuccess:
case IntegrityResult::kSuccessNoFooter:
// Continue loading below.
break;
}
std::optional<base::DictValue> parsed =
base::JSONReader::ReadDict(json_content, base::JSON_PARSE_RFC);
if (!parsed) {
return DatabaseError::kJsonParseError;
}
// Read schema version
std::optional<int> version = parsed->FindInt(kSchemaVersionField);
if (version) {
schema_version_ = *version;
if (schema_version_ > kCurrentSchemaVersion) {
// Schema is newer than we understand. Continue loading known fields
// so the caller can still query, but return kSchemaVersionTooNew
// below so the caller knows not to save (which would drop unknown
// fields from the newer schema).
}
}
// Read apps array
const base::ListValue* apps_list = parsed->FindList(kAppsField);
if (apps_list) {
for (const base::Value& app_value : *apps_list) {
const base::DictValue* app_dict = app_value.GetIfDict();
if (!app_dict) {
continue;
}
AppEntry entry;
const std::string* uuid = app_dict->FindString(kUuidField);
if (!uuid || uuid->empty() || uuid->size() > kMaxUuidLength) {
continue; // Skip invalid entries
}
entry.uuid = *uuid;
const std::string* platform = app_dict->FindString(kPlatformField);
if (!platform || platform->empty() ||
platform->size() > kMaxPlatformLength) {
continue; // Skip entries without valid platform
}
entry.platform = *platform;
const std::string* vmin = app_dict->FindString(kVminField);
if (vmin && vmin->size() > kMaxVersionLength) {
continue; // Skip entries with invalid fields
}
entry.vmin = vmin ? *vmin : "";
const std::string* vmax = app_dict->FindString(kVmaxField);
if (vmax && vmax->size() > kMaxVersionLength) {
continue; // Skip entries with invalid fields
}
entry.vmax = vmax ? *vmax : "";
const std::string* abi_hash = app_dict->FindString(kAbiHashField);
if (abi_hash && abi_hash->size() > kMaxAbiHashLength) {
continue; // Skip entries with invalid fields
}
entry.abi_hash = abi_hash ? *abi_hash : "";
apps_.push_back(std::move(entry));
}
}
// Read pruning suspension timestamp (if present)
auto suspension_value = parsed->Find(kPruningSuspendedUntilField);
if (suspension_value) {
if (suspension_value->is_string()) {
int64_t ts;
if (base::StringToInt64(suspension_value->GetString(), &ts)) {
pruning_suspended_until_ = ts;
}
}
}
// Return error if schema is too new (after loading what we can)
if (schema_version_ > kCurrentSchemaVersion) {
return DatabaseError::kSchemaVersionTooNew;
}
return DatabaseError::kSuccess;
}
DatabaseError Database::Save(const base::FilePath& path) {
if (ShouldFailDatabaseSave()) {
return DatabaseError::kFileWriteError;
}
base::DictValue root;
root.Set(kSchemaVersionField, schema_version_);
if (pruning_suspended_until_ > 0) {
root.Set(kPruningSuspendedUntilField,
base::NumberToString(pruning_suspended_until_));
}
base::ListValue apps_list;
for (const AppEntry& entry : apps_) {
base::DictValue app_dict;
app_dict.Set(kUuidField, entry.uuid);
app_dict.Set(kPlatformField, entry.platform);
app_dict.Set(kVminField, entry.vmin);
if (!entry.vmax.empty()) {
app_dict.Set(kVmaxField, entry.vmax);
}
if (!entry.abi_hash.empty()) {
app_dict.Set(kAbiHashField, entry.abi_hash);
}
apps_list.Append(std::move(app_dict));
}
root.Set(kAppsField, std::move(apps_list));
std::string json_content;
if (!base::JSONWriter::WriteWithOptions(
root, base::JSONWriter::OPTIONS_PRETTY_PRINT, &json_content)) {
return DatabaseError::kFileWriteError;
}
// Write to a temp file then atomically rename to prevent partial writes.
base::FilePath temp_path;
if (!base::CreateTemporaryFileInDir(path.DirName(), &temp_path)) {
return DatabaseError::kFileWriteError;
}
ScopedFileDeleter temp_deleter(temp_path);
if (!WriteFileWithIntegrity(temp_path, json_content)) {
return DatabaseError::kFileWriteError;
}
base::File::Error error;
if (!base::ReplaceFile(temp_path, path, &error)) {
return DatabaseError::kFileWriteError;
}
temp_deleter.Release();
return DatabaseError::kSuccess;
}
int Database::GetSchemaVersion() const {
return schema_version_;
}
bool Database::RegisterApp(const AppEntry& entry) {
// Find existing entry by (uuid, platform)
auto it =
std::find_if(apps_.begin(), apps_.end(), [&entry](const AppEntry& e) {
return e.uuid == entry.uuid && e.platform == entry.platform;
});
if (it != apps_.end()) {
if (*it == entry) {
return false;
}
*it = entry;
} else {
apps_.push_back(entry);
}
return true;
}
void Database::UnregisterApp(const std::string& uuid,
const std::string& platform) {
apps_.erase(std::remove_if(apps_.begin(), apps_.end(),
[&uuid, &platform](const AppEntry& e) {
return e.uuid == uuid && e.platform == platform;
}),
apps_.end());
}
std::optional<AppEntry> Database::GetApp(const std::string& uuid,
const std::string& platform) const {
auto it = std::find_if(apps_.begin(), apps_.end(),
[&uuid, &platform](const AppEntry& e) {
return e.uuid == uuid && e.platform == platform;
});
if (it != apps_.end()) {
return *it;
}
return std::nullopt;
}
const std::vector<AppEntry>& Database::GetAllApps() const {
return apps_;
}
bool Database::IsEmpty() const {
return apps_.empty();
}
Version Database::GetGlobalVmin(const std::string& platform) const {
Version result;
for (const auto& app : apps_) {
if (app.platform != platform) {
continue;
}
Version v = Version::Parse(app.vmin);
if (v.IsValid() && (!result.IsValid() || v < result)) {
result = v;
}
}
return result;
}
bool Database::CanPrune() const {
// Don't prune if schema is newer than we understand.
// This prevents older installers from deleting versions that
// newer schema fields might reference.
if (schema_version_ > kCurrentSchemaVersion) {
return false;
}
// Don't prune while suspension is active. After corruption recovery,
// app entries are lost and pruning would delete versions that other
// (unregistered) apps still need.
if (pruning_suspended_until_ > 0) {
int64_t now = static_cast<int64_t>(time(nullptr));
if (now < pruning_suspended_until_) {
return false;
}
}
return true;
}
void Database::SuspendPruning() {
pruning_suspended_until_ =
static_cast<int64_t>(time(nullptr)) + kPruningSuspensionSeconds;
}
const char* DatabaseErrorToString(DatabaseError error) {
switch (error) {
case DatabaseError::kSuccess:
return "Success";
case DatabaseError::kFileNotFound:
return "Database file not found";
case DatabaseError::kFileReadError:
return "Could not read database file";
case DatabaseError::kFileWriteError:
return "Could not write database file";
case DatabaseError::kJsonParseError:
return "Invalid JSON in database file";
case DatabaseError::kSchemaVersionTooNew:
return "Database schema version too new";
case DatabaseError::kLockAcquisitionFailed:
return "Could not acquire database lock";
case DatabaseError::kIntegrityMismatch:
return "Database file corrupted (CRC32 mismatch, file deleted)";
}
return "Unknown error";
}
} // namespace cef_installer
@@ -0,0 +1,135 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_DATABASE_H_
#define CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_DATABASE_H_
#include <optional>
#include <string>
#include <vector>
#include "base/files/file_path.h"
#include "cef/libcef_dll/bootstrap/installer/installer_file_integrity.h"
#include "cef/libcef_dll/bootstrap/installer/installer_version.h"
namespace cef_installer {
// Current schema version - increment when adding fields.
//
// BACKWARDS COMPATIBILITY:
// Multiple installer versions may share the same database file. Older
// installers must be able to read databases written by newer ones (they
// skip unknown fields) and newer installers must not break older ones.
// Rules:
// - New fields: add with sensible defaults so older readers work without them.
// - Existing fields: never rename, remove, or change semantics.
// - Apps array: per-app entries are re-serialized from AppEntry, so any new
// per-app fields must be added to AppEntry and handled in Load/Save.
// - Increment kCurrentSchemaVersion when adding fields. Older installers
// seeing a higher version will return kSchemaVersionTooNew and skip
// saving (avoiding data loss).
constexpr int kCurrentSchemaVersion = 1;
// Grace period (seconds) before pruning resumes after corruption recovery.
// 7 days gives apps time to re-register on their next launch.
constexpr int64_t kPruningSuspensionSeconds = 7 * 24 * 60 * 60;
// Per-application registration entry
// Unique key is (uuid, platform) - same app on different architectures are
// separate entries.
struct AppEntry {
std::string uuid; // Unique app identifier (never changes)
std::string platform; // Platform identifier (e.g., "windows64", "windows32")
std::string vmin; // Minimum version requirement
std::string vmax; // Maximum version requirement (optional)
std::string abi_hash; // Sandbox compatibility hash
bool operator==(const AppEntry& other) const;
};
// Error codes for database operations
enum class DatabaseError {
kSuccess,
kFileNotFound, // Database file doesn't exist (OK for new install)
kFileReadError, // Could not read database file
kFileWriteError, // Could not write database file
kJsonParseError, // Database file contains invalid JSON
kSchemaVersionTooNew, // Database schema newer than we understand
kLockAcquisitionFailed, // Could not acquire database lock
kIntegrityMismatch, // CRC32 mismatch — corrupted file was deleted
};
// Deterministic save failure for transaction tests. Typed E2E configuration
// maps the matching non-official subprocess fault at this component boundary.
void SetDatabaseSaveFailureForTesting(bool fail);
// Manages the shared installer.json database.
// Thread-safety: Not thread-safe. Use with external locking if needed.
class Database {
public:
Database();
~Database();
Database(const Database&) = delete;
Database& operator=(const Database&) = delete;
// Load database from file. Creates empty database if file doesn't exist.
// If the file has a CRC32 integrity footer and the checksum mismatches,
// |mismatch_action| selects deletion for writer recovery or preservation for
// read-only inspection (returns kIntegrityMismatch either way). Legacy files
// without a footer are accepted.
// Returns kSchemaVersionTooNew if schema_version > kCurrentSchemaVersion.
DatabaseError Load(
const base::FilePath& path,
IntegrityMismatchAction mismatch_action = IntegrityMismatchAction::kDoom);
// Save database to file with CRC32 integrity footer.
// Uses atomic write (temp file + rename) to prevent partial writes.
DatabaseError Save(const base::FilePath& path);
// Get current schema version (from loaded database or kCurrentSchemaVersion)
int GetSchemaVersion() const;
// CRUD operations - keyed by (uuid, platform)
// - Add or update by (uuid, platform)
bool RegisterApp(const AppEntry& entry);
// - Remove by (uuid, platform)
void UnregisterApp(const std::string& uuid, const std::string& platform);
std::optional<AppEntry> GetApp(const std::string& uuid,
const std::string& platform) const;
const std::vector<AppEntry>& GetAllApps() const;
// Check if database is empty (no registered apps)
bool IsEmpty() const;
// Return the minimum vmin across all registered apps for the given
// platform, or an invalid Version if none match.
Version GetGlobalVmin(const std::string& platform) const;
// Check if we can safely prune versions.
// Returns false if schema_version > kCurrentSchemaVersion (skip pruning)
// or if pruning is suspended (grace period after corruption recovery).
bool CanPrune() const;
// Suspend pruning for kPruningSuspensionSeconds from now.
// Called after corruption recovery to give apps time to re-register.
// The suspension timestamp is persisted in the database JSON.
void SuspendPruning();
private:
int schema_version_ = kCurrentSchemaVersion;
std::vector<AppEntry> apps_;
// Unix timestamp (seconds since epoch). Pruning is suspended until this
// time. 0 means not suspended. Persisted as "pruning_suspended_until" in
// the database JSON.
int64_t pruning_suspended_until_ = 0;
};
// Convert error code to human-readable string for logging
const char* DatabaseErrorToString(DatabaseError error);
} // namespace cef_installer
#endif // CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_DATABASE_H_
@@ -0,0 +1,887 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cef/libcef_dll/bootstrap/installer/installer_database.h"
#include <time.h>
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "cef/libcef_dll/bootstrap/installer/installer_file_integrity.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace cef_installer {
namespace {
class InstallerDatabaseTest : public testing::Test {
protected:
void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); }
base::FilePath GetDatabasePath() const {
return temp_dir_.GetPath().Append(L"installer.json");
}
void WriteJsonFile(const std::string& content) {
ASSERT_TRUE(base::WriteFile(GetDatabasePath(), content));
}
base::ScopedTempDir temp_dir_;
};
TEST_F(InstallerDatabaseTest, CreateEmpty) {
Database db;
EXPECT_TRUE(db.IsEmpty());
EXPECT_EQ(kCurrentSchemaVersion, db.GetSchemaVersion());
EXPECT_TRUE(db.CanPrune());
}
TEST_F(InstallerDatabaseTest, SaveEmpty) {
Database db;
DatabaseError error = db.Save(GetDatabasePath());
EXPECT_EQ(DatabaseError::kSuccess, error);
EXPECT_TRUE(base::PathExists(GetDatabasePath()));
// Verify saved content (read through integrity layer to strip footer)
std::string content;
IntegrityResult ir = ReadFileWithIntegrity(GetDatabasePath(), &content);
EXPECT_EQ(IntegrityResult::kSuccess, ir);
EXPECT_NE(content.find("\"schema_version\""), std::string::npos);
EXPECT_NE(content.find("\"apps\""), std::string::npos);
}
TEST_F(InstallerDatabaseTest, LoadNonExistent) {
Database db;
DatabaseError error = db.Load(GetDatabasePath());
// Non-existent file should create empty database
EXPECT_EQ(DatabaseError::kSuccess, error);
EXPECT_TRUE(db.IsEmpty());
}
TEST_F(InstallerDatabaseTest, RegisterAppNew) {
Database db;
AppEntry entry;
entry.uuid = "12345678-1234-1234-1234-123456789abc";
entry.platform = "windows64";
entry.vmin = "137.1";
entry.vmax = "137.99";
entry.abi_hash = "abc123";
EXPECT_TRUE(db.RegisterApp(entry));
EXPECT_FALSE(db.IsEmpty());
auto apps = db.GetAllApps();
ASSERT_EQ(1u, apps.size());
EXPECT_EQ(entry, apps[0]);
}
TEST_F(InstallerDatabaseTest, RegisterAppUpdate) {
Database db;
AppEntry entry1;
entry1.uuid = "12345678-1234-1234-1234-123456789abc";
entry1.platform = "windows64";
entry1.vmin = "137.1";
entry1.vmax = "137.99";
EXPECT_TRUE(db.RegisterApp(entry1));
// Update same (uuid, platform) with different values
AppEntry entry2;
entry2.uuid = "12345678-1234-1234-1234-123456789abc";
entry2.platform = "windows64";
entry2.vmin = "138.0";
entry2.vmax = "138.99";
EXPECT_TRUE(db.RegisterApp(entry2));
auto apps = db.GetAllApps();
ASSERT_EQ(1u, apps.size());
EXPECT_EQ("138.0", apps[0].vmin);
EXPECT_EQ("138.99", apps[0].vmax);
}
TEST_F(InstallerDatabaseTest, RegisterAppUnchanged) {
Database db;
AppEntry entry;
entry.uuid = "12345678-1234-1234-1234-123456789abc";
entry.platform = "windows64";
entry.vmin = "137.1";
entry.vmax = "137.99";
entry.abi_hash = "abc123";
EXPECT_TRUE(db.RegisterApp(entry));
EXPECT_FALSE(db.RegisterApp(entry));
auto apps = db.GetAllApps();
ASSERT_EQ(1u, apps.size());
EXPECT_EQ(entry, apps[0]);
}
TEST_F(InstallerDatabaseTest, RegisterAppDifferentPlatforms) {
Database db;
// Same UUID but different platforms should be separate entries
AppEntry entry1;
entry1.uuid = "12345678-1234-1234-1234-123456789abc";
entry1.platform = "windows64";
entry1.vmin = "137.1";
AppEntry entry2;
entry2.uuid = "12345678-1234-1234-1234-123456789abc";
entry2.platform = "windows32";
entry2.vmin = "138.0";
EXPECT_TRUE(db.RegisterApp(entry1));
EXPECT_TRUE(db.RegisterApp(entry2));
auto apps = db.GetAllApps();
ASSERT_EQ(2u, apps.size());
}
TEST_F(InstallerDatabaseTest, UnregisterApp) {
Database db;
AppEntry entry;
entry.uuid = "12345678-1234-1234-1234-123456789abc";
entry.platform = "windows64";
entry.vmin = "137.1";
db.RegisterApp(entry);
EXPECT_FALSE(db.IsEmpty());
db.UnregisterApp(entry.uuid, entry.platform);
EXPECT_TRUE(db.IsEmpty());
}
TEST_F(InstallerDatabaseTest, UnregisterAppOnlyMatchingPlatform) {
Database db;
// Register same UUID on two platforms
AppEntry entry1;
entry1.uuid = "12345678-1234-1234-1234-123456789abc";
entry1.platform = "windows64";
entry1.vmin = "137.1";
AppEntry entry2;
entry2.uuid = "12345678-1234-1234-1234-123456789abc";
entry2.platform = "windows32";
entry2.vmin = "137.1";
db.RegisterApp(entry1);
db.RegisterApp(entry2);
EXPECT_EQ(2u, db.GetAllApps().size());
// Unregister only windows64 - windows32 should remain
db.UnregisterApp(entry1.uuid, "windows64");
auto apps = db.GetAllApps();
ASSERT_EQ(1u, apps.size());
EXPECT_EQ("windows32", apps[0].platform);
}
TEST_F(InstallerDatabaseTest, GetAppFound) {
Database db;
AppEntry entry;
entry.uuid = "12345678-1234-1234-1234-123456789abc";
entry.platform = "windows64";
entry.vmin = "137.1";
entry.abi_hash = "a1a2a3";
db.RegisterApp(entry);
auto result = db.GetApp(entry.uuid, entry.platform);
ASSERT_TRUE(result.has_value());
EXPECT_EQ(entry, *result);
}
TEST_F(InstallerDatabaseTest, GetAppNotFound) {
Database db;
auto result = db.GetApp("nonexistent-uuid", "windows64");
EXPECT_FALSE(result.has_value());
}
TEST_F(InstallerDatabaseTest, GetAppWrongPlatform) {
Database db;
AppEntry entry;
entry.uuid = "12345678-1234-1234-1234-123456789abc";
entry.platform = "windows64";
entry.vmin = "137.1";
db.RegisterApp(entry);
// Same UUID but different platform should not be found
auto result = db.GetApp(entry.uuid, "windows32");
EXPECT_FALSE(result.has_value());
}
TEST_F(InstallerDatabaseTest, GetAllApps) {
Database db;
AppEntry entry1;
entry1.uuid = "uuid-1";
entry1.platform = "windows64";
entry1.vmin = "137.1";
AppEntry entry2;
entry2.uuid = "uuid-2";
entry2.platform = "windows64";
entry2.vmin = "138.0";
db.RegisterApp(entry1);
db.RegisterApp(entry2);
auto apps = db.GetAllApps();
ASSERT_EQ(2u, apps.size());
}
TEST_F(InstallerDatabaseTest, SchemaVersionCurrent) {
WriteJsonFile(R"({
"schema_version": 1,
"apps": [
{
"uuid": "12345678-1234-1234-1234-123456789abc",
"platform": "windows64",
"vmin": "137.1"
}
]
})");
Database db;
DatabaseError error = db.Load(GetDatabasePath());
EXPECT_EQ(DatabaseError::kSuccess, error);
EXPECT_EQ(1, db.GetSchemaVersion());
EXPECT_TRUE(db.CanPrune());
}
TEST_F(InstallerDatabaseTest, SchemaVersionTooNew) {
WriteJsonFile(R"({
"schema_version": 999,
"apps": [
{
"uuid": "12345678-1234-1234-1234-123456789abc",
"platform": "windows64",
"vmin": "137.1"
}
]
})");
Database db;
DatabaseError error = db.Load(GetDatabasePath());
// Should return error but still load data
EXPECT_EQ(DatabaseError::kSchemaVersionTooNew, error);
EXPECT_EQ(999, db.GetSchemaVersion());
EXPECT_FALSE(db.IsEmpty()); // Data was still loaded
}
TEST_F(InstallerDatabaseTest, CanPruneFalseWhenSchemaTooNew) {
WriteJsonFile(R"({
"schema_version": 999,
"apps": []
})");
Database db;
db.Load(GetDatabasePath());
EXPECT_FALSE(db.CanPrune());
}
TEST_F(InstallerDatabaseTest, JsonRoundTrip) {
Database db1;
AppEntry entry;
entry.uuid = "12345678-1234-1234-1234-123456789abc";
entry.platform = "windows64";
entry.vmin = "137.1";
entry.vmax = "137.99";
entry.abi_hash = "abc123def4567890";
db1.RegisterApp(entry);
ASSERT_EQ(DatabaseError::kSuccess, db1.Save(GetDatabasePath()));
Database db2;
ASSERT_EQ(DatabaseError::kSuccess, db2.Load(GetDatabasePath()));
auto apps = db2.GetAllApps();
ASSERT_EQ(1u, apps.size());
EXPECT_EQ(entry, apps[0]);
}
TEST_F(InstallerDatabaseTest, UnknownFieldsDroppedOnSave) {
// Unknown top-level fields from older schemas are not round-tripped.
// We only save when schema_version <= kCurrentSchemaVersion, so unknown
// fields can only come from older versions and are safe to drop.
WriteJsonFile(R"({
"schema_version": 1,
"apps": [
{
"uuid": "12345678-1234-1234-1234-123456789abc",
"platform": "windows64",
"vmin": "137.1"
}
],
"future_field": "some value",
"another_unknown": 42
})");
Database db;
ASSERT_EQ(DatabaseError::kSuccess, db.Load(GetDatabasePath()));
ASSERT_EQ(DatabaseError::kSuccess, db.Save(GetDatabasePath()));
// Read back and verify unknown fields are not preserved
std::string content;
IntegrityResult ir = ReadFileWithIntegrity(GetDatabasePath(), &content);
EXPECT_EQ(IntegrityResult::kSuccess, ir);
EXPECT_EQ(content.find("future_field"), std::string::npos);
EXPECT_EQ(content.find("another_unknown"), std::string::npos);
}
TEST_F(InstallerDatabaseTest, DatabaseErrorToString) {
EXPECT_STREQ("Success", DatabaseErrorToString(DatabaseError::kSuccess));
EXPECT_STREQ("Database file not found",
DatabaseErrorToString(DatabaseError::kFileNotFound));
EXPECT_STREQ("Could not read database file",
DatabaseErrorToString(DatabaseError::kFileReadError));
EXPECT_STREQ("Could not write database file",
DatabaseErrorToString(DatabaseError::kFileWriteError));
EXPECT_STREQ("Invalid JSON in database file",
DatabaseErrorToString(DatabaseError::kJsonParseError));
EXPECT_STREQ("Database schema version too new",
DatabaseErrorToString(DatabaseError::kSchemaVersionTooNew));
EXPECT_STREQ("Could not acquire database lock",
DatabaseErrorToString(DatabaseError::kLockAcquisitionFailed));
EXPECT_STREQ("Database file corrupted (CRC32 mismatch, file deleted)",
DatabaseErrorToString(DatabaseError::kIntegrityMismatch));
}
// ============================================================================
// Security: Field Length Limit Tests (M2)
// ============================================================================
TEST_F(InstallerDatabaseTest, LoadRejectsOverlyLongUuid) {
// UUID longer than 256 chars should be skipped
std::string long_uuid(300, 'u');
std::string json = R"({
"schema_version": 1,
"apps": [
{
"uuid": ")" + long_uuid +
R"(",
"platform": "windows64",
"vmin": "137.1"
},
{
"uuid": "valid-uuid",
"platform": "windows64",
"vmin": "138.0"
}
]
})";
WriteJsonFile(json);
Database db;
DatabaseError error = db.Load(GetDatabasePath());
EXPECT_EQ(DatabaseError::kSuccess, error);
// Only the valid entry should be loaded
auto apps = db.GetAllApps();
ASSERT_EQ(1u, apps.size());
EXPECT_EQ("valid-uuid", apps[0].uuid);
}
TEST_F(InstallerDatabaseTest, LoadRejectsOverlyLongVmin) {
// vmin longer than 64 chars should cause entry to be skipped
std::string long_version(100, '1');
std::string json = R"({
"schema_version": 1,
"apps": [
{
"uuid": "uuid-1",
"platform": "windows64",
"vmin": ")" + long_version +
R"("
},
{
"uuid": "uuid-2",
"platform": "windows64",
"vmin": "138.0"
}
]
})";
WriteJsonFile(json);
Database db;
DatabaseError error = db.Load(GetDatabasePath());
EXPECT_EQ(DatabaseError::kSuccess, error);
// Only the valid entry should be loaded
auto apps = db.GetAllApps();
ASSERT_EQ(1u, apps.size());
EXPECT_EQ("uuid-2", apps[0].uuid);
}
TEST_F(InstallerDatabaseTest, LoadRejectsOverlyLongVmax) {
// vmax longer than 64 chars should cause entry to be skipped
std::string long_version(100, '9');
std::string json = R"({
"schema_version": 1,
"apps": [
{
"uuid": "uuid-1",
"platform": "windows64",
"vmin": "137.0",
"vmax": ")" + long_version +
R"("
},
{
"uuid": "uuid-2",
"platform": "windows64",
"vmin": "138.0"
}
]
})";
WriteJsonFile(json);
Database db;
DatabaseError error = db.Load(GetDatabasePath());
EXPECT_EQ(DatabaseError::kSuccess, error);
// Only the valid entry should be loaded
auto apps = db.GetAllApps();
ASSERT_EQ(1u, apps.size());
EXPECT_EQ("uuid-2", apps[0].uuid);
}
TEST_F(InstallerDatabaseTest, LoadRejectsOverlyLongAbiHash) {
// abi_hash longer than 256 chars should cause entry to be skipped
std::string long_hash(300, 'h');
std::string json = R"({
"schema_version": 1,
"apps": [
{
"uuid": "uuid-1",
"platform": "windows64",
"vmin": "137.0",
"abi_hash": ")" +
long_hash + R"("
},
{
"uuid": "uuid-2",
"platform": "windows64",
"vmin": "138.0"
}
]
})";
WriteJsonFile(json);
Database db;
DatabaseError error = db.Load(GetDatabasePath());
EXPECT_EQ(DatabaseError::kSuccess, error);
// Only the valid entry should be loaded
auto apps = db.GetAllApps();
ASSERT_EQ(1u, apps.size());
EXPECT_EQ("uuid-2", apps[0].uuid);
}
TEST_F(InstallerDatabaseTest, LoadAcceptsValidFieldLengths) {
// Fields at exactly the limit should be accepted
std::string uuid_256(256, 'u');
std::string version_64(64, '1');
std::string hash_256(256, 'h');
std::string json = R"({
"schema_version": 1,
"apps": [
{
"uuid": ")" + uuid_256 +
R"(",
"platform": "windows64",
"vmin": ")" + version_64 +
R"(",
"abi_hash": ")" +
hash_256 + R"("
}
]
})";
WriteJsonFile(json);
Database db;
DatabaseError error = db.Load(GetDatabasePath());
EXPECT_EQ(DatabaseError::kSuccess, error);
auto apps = db.GetAllApps();
ASSERT_EQ(1u, apps.size());
EXPECT_EQ(uuid_256, apps[0].uuid);
}
TEST_F(InstallerDatabaseTest, LoadRejectsOverlyLongPlatform) {
// platform longer than 32 chars should cause entry to be skipped
std::string long_platform(50, 'p');
std::string json = R"({
"schema_version": 1,
"apps": [
{
"uuid": "uuid-1",
"platform": ")" +
long_platform +
R"(",
"vmin": "137.0"
},
{
"uuid": "uuid-2",
"platform": "windows64",
"vmin": "138.0"
}
]
})";
WriteJsonFile(json);
Database db;
DatabaseError error = db.Load(GetDatabasePath());
EXPECT_EQ(DatabaseError::kSuccess, error);
// Only the valid entry should be loaded
auto apps = db.GetAllApps();
ASSERT_EQ(1u, apps.size());
EXPECT_EQ("uuid-2", apps[0].uuid);
}
TEST_F(InstallerDatabaseTest, LoadRejectsMissingPlatform) {
// Entry without platform field should be skipped
std::string json = R"({
"schema_version": 1,
"apps": [
{
"uuid": "uuid-1",
"vmin": "137.0"
},
{
"uuid": "uuid-2",
"platform": "windows64",
"vmin": "138.0"
}
]
})";
WriteJsonFile(json);
Database db;
DatabaseError error = db.Load(GetDatabasePath());
EXPECT_EQ(DatabaseError::kSuccess, error);
// Only the valid entry should be loaded
auto apps = db.GetAllApps();
ASSERT_EQ(1u, apps.size());
EXPECT_EQ("uuid-2", apps[0].uuid);
}
// ============================================================================
// Integrity & Atomic Write Tests
// ============================================================================
TEST_F(InstallerDatabaseTest, SaveWritesIntegrityFooter) {
Database db;
AppEntry entry;
entry.uuid = "test-uuid";
entry.platform = "windows64";
entry.vmin = "137.1";
db.RegisterApp(entry);
ASSERT_EQ(DatabaseError::kSuccess, db.Save(GetDatabasePath()));
// File should have a valid integrity footer
std::string content;
IntegrityResult ir = ReadFileWithIntegrity(GetDatabasePath(), &content);
EXPECT_EQ(IntegrityResult::kSuccess, ir);
EXPECT_NE(content.find("test-uuid"), std::string::npos);
}
TEST_F(InstallerDatabaseTest, LoadAcceptsLegacyFileWithoutFooter) {
// Legacy files (written without integrity footer) should still load
WriteJsonFile(R"({
"schema_version": 1,
"apps": [
{
"uuid": "legacy-uuid",
"platform": "windows64",
"vmin": "137.1"
}
]
})");
Database db;
DatabaseError error = db.Load(GetDatabasePath());
EXPECT_EQ(DatabaseError::kSuccess, error);
auto apps = db.GetAllApps();
ASSERT_EQ(1u, apps.size());
EXPECT_EQ("legacy-uuid", apps[0].uuid);
}
TEST_F(InstallerDatabaseTest, LoadDetectsCorruption) {
// Write a valid file with integrity footer
Database db;
AppEntry entry;
entry.uuid = "test-uuid";
entry.platform = "windows64";
entry.vmin = "137.1";
db.RegisterApp(entry);
ASSERT_EQ(DatabaseError::kSuccess, db.Save(GetDatabasePath()));
// Corrupt the file by flipping a byte in the content
std::string raw;
ASSERT_TRUE(base::ReadFileToString(GetDatabasePath(), &raw));
ASSERT_GT(raw.size(), 20u);
raw[5] ^= 0xFF; // Flip a byte in the JSON content
ASSERT_TRUE(base::WriteFile(GetDatabasePath(), raw));
// Load should detect corruption, delete the file, return empty DB
Database db2;
DatabaseError error = db2.Load(GetDatabasePath());
EXPECT_EQ(DatabaseError::kIntegrityMismatch, error);
EXPECT_TRUE(db2.IsEmpty());
EXPECT_FALSE(base::PathExists(GetDatabasePath()));
}
TEST_F(InstallerDatabaseTest, RetentionReadPreservesCorruption) {
Database database;
database.RegisterApp({"test-uuid", "windows64", "137.1", "", ""});
ASSERT_EQ(DatabaseError::kSuccess, database.Save(GetDatabasePath()));
std::string raw;
ASSERT_TRUE(base::ReadFileToString(GetDatabasePath(), &raw));
ASSERT_GT(raw.size(), 20u);
raw[5] ^= 0xFF;
ASSERT_TRUE(base::WriteFile(GetDatabasePath(), raw));
Database inspected;
EXPECT_EQ(
DatabaseError::kIntegrityMismatch,
inspected.Load(GetDatabasePath(), IntegrityMismatchAction::kPreserve));
std::string after;
ASSERT_TRUE(base::ReadFileToString(GetDatabasePath(), &after));
EXPECT_EQ(raw, after);
}
TEST_F(InstallerDatabaseTest, SaveThenLoadRoundTripsWithIntegrity) {
Database db1;
AppEntry entry;
entry.uuid = "round-trip-uuid";
entry.platform = "windows64";
entry.vmin = "138.0";
entry.vmax = "138.99";
entry.abi_hash = "abc123";
db1.RegisterApp(entry);
ASSERT_EQ(DatabaseError::kSuccess, db1.Save(GetDatabasePath()));
Database db2;
ASSERT_EQ(DatabaseError::kSuccess, db2.Load(GetDatabasePath()));
auto apps = db2.GetAllApps();
ASSERT_EQ(1u, apps.size());
EXPECT_EQ(entry, apps[0]);
}
TEST_F(InstallerDatabaseTest, DeterministicSaveFailure) {
Database database;
SetDatabaseSaveFailureForTesting(true);
EXPECT_EQ(DatabaseError::kFileWriteError, database.Save(GetDatabasePath()));
SetDatabaseSaveFailureForTesting(false);
EXPECT_FALSE(base::PathExists(GetDatabasePath()));
}
// ============================================================================
// Pruning Suspension Tests
// ============================================================================
TEST_F(InstallerDatabaseTest, CanPruneDefaultTrue) {
Database db;
EXPECT_TRUE(db.CanPrune());
}
TEST_F(InstallerDatabaseTest, SuspendPruningPreventsCanPrune) {
Database db;
db.SuspendPruning();
EXPECT_FALSE(db.CanPrune());
}
TEST_F(InstallerDatabaseTest, SuspendPruningPersistsAcrossSaveLoad) {
Database db1;
db1.SuspendPruning();
ASSERT_EQ(DatabaseError::kSuccess, db1.Save(GetDatabasePath()));
Database db2;
ASSERT_EQ(DatabaseError::kSuccess, db2.Load(GetDatabasePath()));
EXPECT_FALSE(db2.CanPrune());
}
TEST_F(InstallerDatabaseTest, ExpiredSuspensionAllowsPruning) {
// Write a DB with a suspension timestamp in the past
int64_t past = static_cast<int64_t>(time(nullptr)) - 100;
std::string json = R"({
"schema_version": 1,
"pruning_suspended_until": ")" +
std::to_string(past) + R"(",
"apps": []
})";
WriteJsonFile(json);
Database db;
ASSERT_EQ(DatabaseError::kSuccess, db.Load(GetDatabasePath()));
EXPECT_TRUE(db.CanPrune());
}
TEST_F(InstallerDatabaseTest, ActiveSuspensionBlocksPruning) {
// Write a DB with a suspension timestamp in the future
int64_t future = static_cast<int64_t>(time(nullptr)) + 86400;
std::string json = R"({
"schema_version": 1,
"pruning_suspended_until": ")" +
std::to_string(future) + R"(",
"apps": []
})";
WriteJsonFile(json);
Database db;
ASSERT_EQ(DatabaseError::kSuccess, db.Load(GetDatabasePath()));
EXPECT_FALSE(db.CanPrune());
}
TEST_F(InstallerDatabaseTest, SuspensionFieldNotWrittenWhenZero) {
Database db;
ASSERT_EQ(DatabaseError::kSuccess, db.Save(GetDatabasePath()));
std::string content;
IntegrityResult ir = ReadFileWithIntegrity(GetDatabasePath(), &content);
EXPECT_EQ(IntegrityResult::kSuccess, ir);
EXPECT_EQ(content.find("pruning_suspended_until"), std::string::npos);
}
// ============================================================================
// Coverage gap tests
// ============================================================================
TEST_F(InstallerDatabaseTest, LoadFileReadError) {
// Create a directory at the database path — reading it as a file fails.
ASSERT_TRUE(base::CreateDirectory(GetDatabasePath()));
Database db;
DatabaseError error = db.Load(GetDatabasePath());
EXPECT_EQ(DatabaseError::kFileReadError, error);
}
TEST_F(InstallerDatabaseTest, LoadInvalidJsonLegacyFile) {
// Legacy file (no integrity footer) with invalid JSON.
WriteJsonFile("this is not valid json {{{");
Database db;
DatabaseError error = db.Load(GetDatabasePath());
EXPECT_EQ(DatabaseError::kJsonParseError, error);
}
TEST_F(InstallerDatabaseTest, LoadMissingAppsArray) {
// Valid JSON but no "apps" key — should load with empty app list.
WriteJsonFile(R"({"schema_version": 1})");
Database db;
DatabaseError error = db.Load(GetDatabasePath());
EXPECT_EQ(DatabaseError::kSuccess, error);
EXPECT_TRUE(db.IsEmpty());
}
TEST_F(InstallerDatabaseTest, LoadNonDictAppEntry) {
// Apps array contains a non-object entry — should be skipped.
WriteJsonFile(R"({
"schema_version": 1,
"apps": [
"not an object",
{
"uuid": "valid-uuid",
"platform": "windows64",
"vmin": "137.1"
}
]
})");
Database db;
DatabaseError error = db.Load(GetDatabasePath());
EXPECT_EQ(DatabaseError::kSuccess, error);
auto apps = db.GetAllApps();
ASSERT_EQ(1u, apps.size());
EXPECT_EQ("valid-uuid", apps[0].uuid);
}
TEST_F(InstallerDatabaseTest, GetGlobalVmin_Empty) {
Database db;
EXPECT_FALSE(db.GetGlobalVmin("windows64").IsValid());
}
TEST_F(InstallerDatabaseTest, GetGlobalVmin_SingleApp) {
Database db;
AppEntry entry;
entry.uuid = "app1";
entry.platform = "windows64";
entry.vmin = "150.1.0";
db.RegisterApp(entry);
Version vmin = db.GetGlobalVmin("windows64");
EXPECT_TRUE(vmin.IsValid());
EXPECT_EQ("150.1.0", vmin.ToString());
}
TEST_F(InstallerDatabaseTest, GetGlobalVmin_MultipleApps) {
Database db;
AppEntry e1;
e1.uuid = "app1";
e1.platform = "windows64";
e1.vmin = "150.2.0";
db.RegisterApp(e1);
AppEntry e2;
e2.uuid = "app2";
e2.platform = "windows64";
e2.vmin = "149.1.0";
db.RegisterApp(e2);
AppEntry e3;
e3.uuid = "app3";
e3.platform = "windows64";
e3.vmin = "151.0.0";
db.RegisterApp(e3);
Version vmin = db.GetGlobalVmin("windows64");
EXPECT_TRUE(vmin.IsValid());
EXPECT_EQ("149.1.0", vmin.ToString());
}
TEST_F(InstallerDatabaseTest, GetGlobalVmin_FiltersPlatform) {
Database db;
AppEntry e1;
e1.uuid = "app1";
e1.platform = "windows64";
e1.vmin = "150.0.0";
db.RegisterApp(e1);
AppEntry e2;
e2.uuid = "app2";
e2.platform = "windowsarm64";
e2.vmin = "148.0.0";
db.RegisterApp(e2);
EXPECT_EQ("150.0.0", db.GetGlobalVmin("windows64").ToString());
EXPECT_EQ("148.0.0", db.GetGlobalVmin("windowsarm64").ToString());
EXPECT_FALSE(db.GetGlobalVmin("linux64").IsValid());
}
} // namespace
} // namespace cef_installer
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,331 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_DOWNLOAD_H_
#define CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_DOWNLOAD_H_
#include <string>
#include <string_view>
#include "base/files/file_path.h"
#include "base/functional/callback.h"
#include "base/time/time.h"
#include "cef/libcef_dll/bootstrap/installer/installer_constants.h"
namespace cef_installer {
// Download progress callback.
// Parameters: bytes_downloaded, total_bytes (0 if unknown)
// Return false to cancel the download.
using DownloadProgressCallback =
base::RepeatingCallback<bool(uint64_t, uint64_t)>;
// Error codes for download operations.
enum class DownloadError {
kSuccess,
kInvalidUrl, // URL is malformed or not HTTPS
kNetworkError, // Connection failed, timeout, etc.
kHttpError, // Server returned error status (4xx, 5xx)
kFileWriteError, // Could not write to destination
kHashMismatch, // Downloaded file hash doesn't match expected
kCancelled, // Download cancelled via callback
kFileTooLarge, // Downloaded file exceeds max_download_size
kProtocolError, // Invalid Range/response protocol
};
// Default maximum download size (500 MB for archives)
constexpr uint64_t kDefaultMaxDownloadSize = 500 * 1024 * 1024;
// Default maximum size for in-memory downloads (10 MB for manifests, hashes)
constexpr uint64_t kDefaultMaxStringDownloadSize = 10 * 1024 * 1024;
// One clean full-request retry shared by all configured origins for one
// archive candidate. It is consumed by redirect-origin changes that need a
// new request, invalid Range responses, final-size mismatch, or hash mismatch.
struct CleanDownloadRetryBudget {
bool consumed = false;
bool TryConsume() {
if (consumed) {
return false;
}
consumed = true;
return true;
}
};
// Download options.
struct DownloadOptions {
// Expected SHA256 hash of the file (hex string, 64 chars).
// If non-empty, downloaded file is verified against this hash.
std::string expected_sha256;
// Expected SHA1 hash (hex string, 40 chars) - alternative to SHA256.
std::string expected_sha1;
// Progress callback (optional).
DownloadProgressCallback progress_callback;
// Maximum download size in bytes. Downloads exceeding this limit are rejected
// to prevent disk exhaustion attacks. Set to 0 to disable the limit.
// Default: 500 MB.
uint64_t max_download_size = kDefaultMaxDownloadSize;
// TEST ONLY: Allow HTTP URLs for testing with local test servers.
// In production, only HTTPS is allowed. This flag should never be set
// outside of unit tests.
bool allow_http_for_testing = false;
// TEST ONLY: Ignore certificate errors for loopback HTTPS test servers.
// Production callers must leave this false; public configuration continues
// to require normally trusted HTTPS endpoints.
bool ignore_certificate_errors_for_testing = false;
// Override for the WinHTTP receive-response timeout (milliseconds).
// 0 means use the default (60 s). Shorter values let cancellation-aware
// callers avoid blocking for the full default timeout when the server is
// unresponsive.
int receive_timeout_ms = 0;
// Override for DNS resolution and connection timeouts (milliseconds).
// 0 means use the defaults. Used with receive_timeout_ms to bound
// launch-path revocation refresh.
int connect_timeout_ms = 0;
// Path to a local directory containing CDN-structured files.
// When set, DownloadFile and DownloadWithCache read/copy files from this
// directory instead of fetching from the network. The filename is extracted
// from the URL path (last component). All usual validation (hash
// verification, size limits) is still performed.
base::FilePath local_download_path;
// Enable origin-bound resumable transfer for a hash-addressed archive.
// Small in-memory downloads and ordinary file downloads leave this false.
bool enable_resume = false;
// Optional candidate-wide retry budget. When null, DownloadFile owns a
// per-call budget. The pointed-to value must outlive DownloadFile.
CleanDownloadRetryBudget* clean_retry_budget = nullptr;
// Optional response-status output for callers that must distinguish archive
// propagation lag (404) from a general origin failure. Set only after
// response headers are available; never used for policy decisions.
int* response_status_code = nullptr;
};
// Download a file from URL to destination path.
// - URL must be HTTPS (HTTP URLs are rejected for security)
// - Creates parent directories if needed
// - Downloads to a temp file first, then renames on success
// - Verifies hash if expected_sha256 or expected_sha1 is provided
DownloadError DownloadFile(const std::string& url,
const base::FilePath& dest_path,
const DownloadOptions& options = {});
// Safely removes deterministic partial variants for one hash-keyed archive.
// Used before configured-origin failover when sidecars change the hash path.
DownloadError DiscardDownloadPartials(const base::FilePath& dest_path);
// Download a file and return its contents as a string.
// Useful for small files like manifest JSON or hash files.
// Same security requirements as DownloadFile.
// Prefer DownloadWithCache() for repeated downloads of the same URL.
DownloadError DownloadToString(const std::string& url,
std::string* content,
const DownloadOptions& options = {});
// Download a small response using asynchronous WinHTTP with a cancellable
// absolute deadline. This is used by automatic-startup revocation refresh so
// DNS, connection, response headers, and response body share one wall-clock
// budget. |overall_timeout| must be positive. Progress callbacks are not
// supported because blocking caller code cannot participate in the network
// deadline; a configured callback returns kCancelled before network activity.
DownloadError DownloadToStringWithDeadline(const std::string& url,
std::string* content,
base::TimeDelta overall_timeout,
const DownloadOptions& options = {});
// Compute SHA256 hash of a file (returns lowercase hex string, 64 chars).
// Returns empty string on error.
std::string ComputeFileSha256(const base::FilePath& path);
// Compute SHA1 hash of a file (returns lowercase hex string, 40 chars).
// Returns empty string on error.
std::string ComputeFileSha1(const base::FilePath& path);
// Verify a file's hash against expected value.
// Checks SHA256 first if provided, then SHA1.
// Returns true if at least one hash matches.
bool VerifyFileHash(const base::FilePath& path,
const std::string& expected_sha256,
const std::string& expected_sha1);
// ============================================================================
// Cache Management for CDN Manifest Files
// ============================================================================
// Get the cache directory path: <install_dir>/.cache/
base::FilePath GetCacheDirectory(const base::FilePath& install_dir);
// Get the cached file path for a URL.
// Converts URL to a safe filename: SHA256(url).cache
base::FilePath GetCacheFilePath(const base::FilePath& cache_dir,
const std::string& url);
// Check if a cached file is still valid (not expired).
// Returns true if file exists, is not a reparse point, and was modified
// within |max_age_seconds| (defaults to kManifestCacheValiditySeconds).
// Optionally returns the file's last-modified time for reuse (e.g. for
// If-Modified-Since headers).
// Note: uses a fixed expiry rather than CDN cache-control headers to avoid
// the complexity of HTTP header parsing. This is a reasonable approximation
// for the small files being cached.
bool IsCacheValid(const base::FilePath& cache_path,
base::Time* out_last_modified = nullptr,
int max_age_seconds = kManifestCacheValiditySeconds);
enum class StaleCacheFallback {
kAllow,
kSkip,
};
enum class CacheWriteBehavior {
kAutomatic,
kDefer,
};
enum class DownloadContentSource {
kNetwork,
kCache,
};
// Reads an integrity-valid cache entry without performing network or creating
// cache directories. |cache_key| is the source-neutral request identity used
// when the entry was written.
DownloadError ReadDownloadCache(const base::FilePath& cache_dir,
std::string_view cache_key,
std::string* content);
// Promotes artifact-specific validated content to a source-neutral cache
// entry. Callers must validate |content| before calling this function.
DownloadError WriteDownloadCache(const base::FilePath& cache_dir,
std::string_view cache_key,
std::string_view content);
// Discards a cache entry that failed artifact-specific validation.
bool DiscardDownloadCache(const base::FilePath& cache_dir,
std::string_view cache_key);
// Download a file with CDN-first caching.
//
// CDN-first flow (when cache exists and is within validity window):
// 1. Send HEAD request with If-Modified-Since to validate cache with CDN
// 2. If 304 Not Modified: serve from cache (CDN-validated)
// 3. If 200 OK: download fresh content and update cache
// 4. If network error: fall back to cache (offline fallback)
//
// When cache is missing, expired, or force_check is true:
// 1. Download fresh content from CDN
// 2. On network failure: try serving from cache as last resort
//
// This minimizes the window where tampered cache content could be served
// to only when the CDN is unreachable.
//
// Set |stale_cache_fallback| to kSkip while trying ordered origins, then call
// ReadDownloadCache() once after all origins fail. A non-empty |cache_key|
// gives those origins one source-neutral cache identity. kDefer preserves
// fresh-cache HEAD/304 behavior but leaves network content uncommitted until
// the caller validates and promotes it with WriteDownloadCache().
DownloadError DownloadWithCache(
const std::string& url,
const base::FilePath& cache_dir,
std::string* content,
const DownloadOptions& options = {},
bool force_check = false,
StaleCacheFallback stale_cache_fallback = StaleCacheFallback::kAllow,
std::string_view cache_key = {},
CacheWriteBehavior cache_write_behavior = CacheWriteBehavior::kAutomatic,
DownloadContentSource* content_source = nullptr);
// Clean up expired cache files.
void PruneCacheDirectory(const base::FilePath& cache_dir);
// Convert error code to human-readable string for logging.
const char* DownloadErrorToString(DownloadError error);
// Check if a URL is valid for downloading (must be HTTPS).
bool IsValidDownloadUrl(const std::string& url);
// ============================================================================
// Internal helpers exposed for testing. Not part of the public API.
// ============================================================================
namespace internal {
// Parsed URL components for WinHTTP.
struct UrlComponents {
std::wstring host;
std::wstring path;
uint16_t port = 0;
bool is_https = false;
};
struct ContentRange {
uint64_t start = 0;
uint64_t end = 0;
uint64_t total = 0;
};
// Strictly parses one complete byte Content-Range value and requires its start
// to equal |expected_start|. Wildcard totals, extra whitespace/ranges,
// overflow, contradictions, and totals above |max_size| are rejected.
bool ParseContentRange(std::string_view value,
uint64_t expected_start,
uint64_t max_size,
ContentRange* output);
// Normalizes a URL origin as lower-case scheme/host plus effective port.
// Paths, queries, fragments, and credentials are never returned.
bool NormalizeUrlOrigin(const std::string& url, std::string* origin);
// Returns the deterministic configured/final-origin-bound partial path used by
// resumable archive downloads. Empty indicates invalid input.
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
base::FilePath GetDownloadPartialPathForTesting(
const base::FilePath& destination,
const std::string& configured_url,
const std::string& final_url);
// Prunes against an injected wall-clock value so archive TTL boundaries are
// deterministic and do not depend on test execution timing.
void PruneCacheDirectoryAtTimeForTesting(const base::FilePath& cache_dir,
base::Time now);
#endif
// Parse a URL into components for WinHTTP.
bool ParseUrl(const std::string& url, UrlComponents* out);
// Format a base::Time as an HTTP-date string (RFC 7231) for If-Modified-Since.
// Example: "Sat, 08 Mar 2026 12:00:00 GMT"
std::wstring FormatHttpDate(const base::Time& time);
// Result of a HEAD request for CDN cache validation.
enum class HeadResult {
kNotModified, // 304: cache is current
kModified, // 200: CDN has newer content
kNetworkError, // Could not reach CDN
};
// Perform a HEAD request with If-Modified-Since to validate cache freshness.
// When options.local_download_path is set, skips the network request and
// returns kModified so the caller always fetches fresh content from the local
// directory.
HeadResult HeadRequest(const std::string& url,
const base::Time& cache_time,
const DownloadOptions& options);
} // namespace internal
} // namespace cef_installer
#endif // CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_DOWNLOAD_H_
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,148 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cef/libcef_dll/bootstrap/installer/installer_e2e_config.h"
#include <windows.h>
#include <string_view>
#include <utility>
#include "base/environment.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/time/time.h"
namespace cef_installer::internal {
namespace {
constexpr base::TimeDelta kChildStateBarrierTimeout = base::Seconds(30);
InstallerE2EConfig LoadInstallerE2EConfigFromEnvironment() {
InstallerE2EConfig config;
auto environment = base::Environment::Create();
const std::optional<std::string> directory_scenario =
environment->GetVar("CEF_INSTALLER_TEST_DIRECTORY_SCENARIO");
const std::optional<std::string> directory_root =
environment->GetVar("CEF_INSTALLER_TEST_DIRECTORY_ROOT");
#if defined(OFFICIAL_BUILD) && defined(NDEBUG)
if (directory_scenario || directory_root) {
config.directory_override = InstallerE2EDirectoryOverride::kInvalid;
}
return config;
#else
if (directory_scenario || directory_root) {
if (!directory_scenario || !directory_root ||
*directory_scenario != "admin_mutation_denied") {
config.directory_override = InstallerE2EDirectoryOverride::kInvalid;
} else {
config.directory_root = base::FilePath::FromUTF8Unsafe(*directory_root)
.StripTrailingSeparators();
config.directory_override =
config.directory_root.IsAbsolute() &&
!config.directory_root.ReferencesParent()
? InstallerE2EDirectoryOverride::kAdminMutationDenied
: InstallerE2EDirectoryOverride::kInvalid;
}
}
if (const auto value =
environment->GetVar("CEF_INSTALLER_TEST_FILE_OPS_FAULT")) {
if (*value == "quarantine_move") {
config.file_ops_fault = InstallerE2EFileOpsFault::kQuarantineMove;
} else if (*value == "repair_move") {
config.file_ops_fault = InstallerE2EFileOpsFault::kRepairMove;
} else if (*value == "trash_move") {
config.file_ops_fault = InstallerE2EFileOpsFault::kTrashMove;
} else if (*value == "trash_reclaim") {
config.file_ops_fault = InstallerE2EFileOpsFault::kTrashReclaim;
}
}
if (const auto value =
environment->GetVar("CEF_INSTALLER_TEST_INDEX_FAULT")) {
if (*value == "write") {
config.version_index_fault = InstallerE2EVersionIndexFault::kWrite;
} else if (*value == "replace") {
config.version_index_fault = InstallerE2EVersionIndexFault::kReplace;
} else if (*value == "reread") {
config.version_index_fault = InstallerE2EVersionIndexFault::kReread;
} else if (*value == "validation") {
config.version_index_fault = InstallerE2EVersionIndexFault::kValidation;
}
}
const auto enabled = [&environment](base::cstring_view name) {
const auto value = environment->GetVar(name);
return value && *value == "1";
};
config.database_save_failure =
enabled("CEF_INSTALLER_TEST_DATABASE_SAVE_FAILURE");
config.relaunch_failure = enabled("CEF_INSTALLER_TEST_RELAUNCH_FAILURE");
config.child_config_failure =
enabled("CEF_INSTALLER_TEST_CHILD_CONFIG_FAILURE");
if (const auto value =
environment->GetVar("CEF_INSTALLER_TEST_CHILD_STATE_BARRIER")) {
base::FilePath path = base::FilePath::FromUTF8Unsafe(*value);
if (path.IsAbsolute() && !path.ReferencesParent()) {
config.child_state_barrier = std::move(path);
}
}
return config;
#endif
}
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
std::optional<InstallerE2EConfig>& ConfigForTesting() {
static base::NoDestructor<std::optional<InstallerE2EConfig>> config;
return *config;
}
#endif
} // namespace
const InstallerE2EConfig& GetInstallerE2EConfig() {
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
if (ConfigForTesting()) {
return *ConfigForTesting();
}
#endif
static const base::NoDestructor<InstallerE2EConfig> config(
LoadInstallerE2EConfigFromEnvironment());
return *config;
}
void WaitAtInstallerE2EChildStateBarrier() {
const base::FilePath& path = GetInstallerE2EConfig().child_state_barrier;
if (path.empty() || !base::WriteFile(path, "ready")) {
return;
}
const base::TimeTicks deadline =
base::TimeTicks::Now() + kChildStateBarrierTimeout;
while (base::PathExists(path) && base::TimeTicks::Now() < deadline) {
::Sleep(25);
}
if (base::PathExists(path)) {
LOG(ERROR) << "Timed out waiting at installer E2E child-state barrier";
}
}
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
InstallerE2EConfig LoadInstallerE2EConfigFromEnvironmentForTesting() {
return LoadInstallerE2EConfigFromEnvironment();
}
void SetInstallerE2EConfigForTesting(
const std::optional<InstallerE2EConfig>& config) {
ConfigForTesting() = config;
}
#endif
} // namespace cef_installer::internal
@@ -0,0 +1,67 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_E2E_CONFIG_H_
#define CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_E2E_CONFIG_H_
#include <optional>
#include "base/files/file_path.h"
namespace cef_installer::internal {
enum class InstallerE2EDirectoryOverride {
kNone,
kAdminMutationDenied,
kInvalid,
};
enum class InstallerE2EFileOpsFault {
kNone,
kQuarantineMove,
kRepairMove,
kTrashMove,
kTrashReclaim,
};
enum class InstallerE2EVersionIndexFault {
kNone,
kWrite,
kReplace,
kReread,
kValidation,
};
// Typed, process-wide configuration for subprocess/E2E-only fault injection.
// Environment variables are parsed only here; product components consume
// these values at the boundary that owns the simulated failure.
struct InstallerE2EConfig {
InstallerE2EDirectoryOverride directory_override =
InstallerE2EDirectoryOverride::kNone;
base::FilePath directory_root;
InstallerE2EFileOpsFault file_ops_fault = InstallerE2EFileOpsFault::kNone;
InstallerE2EVersionIndexFault version_index_fault =
InstallerE2EVersionIndexFault::kNone;
bool database_save_failure = false;
bool relaunch_failure = false;
bool child_config_failure = false;
base::FilePath child_state_barrier;
};
const InstallerE2EConfig& GetInstallerE2EConfig();
// Signals and waits at the trusted-child-state checkpoint when configured.
// The wait is bounded so abandoned test state cannot stall the process
// indefinitely.
void WaitAtInstallerE2EChildStateBarrier();
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
InstallerE2EConfig LoadInstallerE2EConfigFromEnvironmentForTesting();
void SetInstallerE2EConfigForTesting(
const std::optional<InstallerE2EConfig>& config);
#endif
} // namespace cef_installer::internal
#endif // CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_E2E_CONFIG_H_
@@ -0,0 +1,108 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cef/libcef_dll/bootstrap/installer/installer_e2e_config.h"
#include "base/files/scoped_temp_dir.h"
#include "base/scoped_environment_variable_override.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace cef_installer::internal {
namespace {
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
TEST(InstallerE2EConfigTest, ParsesTypedEnvironmentConfiguration) {
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
const base::FilePath barrier = temp_dir.GetPath().Append(L"barrier");
base::ScopedEnvironmentVariableOverride directory_scenario(
"CEF_INSTALLER_TEST_DIRECTORY_SCENARIO", "admin_mutation_denied");
base::ScopedEnvironmentVariableOverride directory_root(
"CEF_INSTALLER_TEST_DIRECTORY_ROOT", temp_dir.GetPath().AsUTF8Unsafe());
base::ScopedEnvironmentVariableOverride file_ops(
"CEF_INSTALLER_TEST_FILE_OPS_FAULT", "trash_reclaim");
base::ScopedEnvironmentVariableOverride version_index(
"CEF_INSTALLER_TEST_INDEX_FAULT", "validation");
base::ScopedEnvironmentVariableOverride database(
"CEF_INSTALLER_TEST_DATABASE_SAVE_FAILURE", "1");
base::ScopedEnvironmentVariableOverride relaunch(
"CEF_INSTALLER_TEST_RELAUNCH_FAILURE", "1");
base::ScopedEnvironmentVariableOverride child_config(
"CEF_INSTALLER_TEST_CHILD_CONFIG_FAILURE", "1");
base::ScopedEnvironmentVariableOverride child_barrier(
"CEF_INSTALLER_TEST_CHILD_STATE_BARRIER", barrier.AsUTF8Unsafe());
const InstallerE2EConfig config =
LoadInstallerE2EConfigFromEnvironmentForTesting();
EXPECT_EQ(InstallerE2EDirectoryOverride::kAdminMutationDenied,
config.directory_override);
EXPECT_EQ(temp_dir.GetPath(), config.directory_root);
EXPECT_EQ(InstallerE2EFileOpsFault::kTrashReclaim, config.file_ops_fault);
EXPECT_EQ(InstallerE2EVersionIndexFault::kValidation,
config.version_index_fault);
EXPECT_TRUE(config.database_save_failure);
EXPECT_TRUE(config.relaunch_failure);
EXPECT_TRUE(config.child_config_failure);
EXPECT_EQ(barrier, config.child_state_barrier);
}
TEST(InstallerE2EConfigTest, RejectsIncompleteDirectoryConfiguration) {
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
base::ScopedEnvironmentVariableOverride directory_root(
"CEF_INSTALLER_TEST_DIRECTORY_ROOT", temp_dir.GetPath().AsUTF8Unsafe());
EXPECT_EQ(
InstallerE2EDirectoryOverride::kInvalid,
LoadInstallerE2EConfigFromEnvironmentForTesting().directory_override);
}
TEST(InstallerE2EConfigTest, ParsesEveryFileOpsFault) {
struct TestCase {
const char* value;
InstallerE2EFileOpsFault expected;
};
const TestCase cases[] = {
{"quarantine_move", InstallerE2EFileOpsFault::kQuarantineMove},
{"repair_move", InstallerE2EFileOpsFault::kRepairMove},
{"trash_move", InstallerE2EFileOpsFault::kTrashMove},
{"trash_reclaim", InstallerE2EFileOpsFault::kTrashReclaim},
{"unknown", InstallerE2EFileOpsFault::kNone},
};
for (const auto& test : cases) {
SCOPED_TRACE(test.value);
base::ScopedEnvironmentVariableOverride fault(
"CEF_INSTALLER_TEST_FILE_OPS_FAULT", test.value);
EXPECT_EQ(test.expected,
LoadInstallerE2EConfigFromEnvironmentForTesting().file_ops_fault);
}
}
TEST(InstallerE2EConfigTest, ParsesEveryVersionIndexFault) {
struct TestCase {
const char* value;
InstallerE2EVersionIndexFault expected;
};
const TestCase cases[] = {
{"write", InstallerE2EVersionIndexFault::kWrite},
{"replace", InstallerE2EVersionIndexFault::kReplace},
{"reread", InstallerE2EVersionIndexFault::kReread},
{"validation", InstallerE2EVersionIndexFault::kValidation},
{"unknown", InstallerE2EVersionIndexFault::kNone},
};
for (const auto& test : cases) {
SCOPED_TRACE(test.value);
base::ScopedEnvironmentVariableOverride fault(
"CEF_INSTALLER_TEST_INDEX_FAULT", test.value);
EXPECT_EQ(
test.expected,
LoadInstallerE2EConfigFromEnvironmentForTesting().version_index_fault);
}
}
#endif
} // namespace
} // namespace cef_installer::internal
@@ -0,0 +1,479 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cef/libcef_dll/bootstrap/installer/installer_file_integrity.h"
#include <string.h>
#include <limits>
#include <type_traits>
#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/functional/callback.h"
#include "base/no_destructor.h"
#include "cef/libcef_dll/bootstrap/installer/installer_constants.h"
#include "cef/libcef_dll/bootstrap/installer/installer_crc.h"
#include "cef/libcef_dll/bootstrap/installer/installer_file_ops.h"
#include "third_party/lzma_sdk/src/C/7zCrc.h"
namespace cef_installer {
namespace {
constexpr uint64_t kIntegrityFooterMagic = UINT64_C(0xCEF09E1F4A3B2C1D);
// Footer appended to files written with WriteFileWithIntegrity.
// Matches the on-disk layout exactly (no implicit padding).
struct IntegrityFooter {
uint32_t data_crc32; // CRC32 of content preceding the footer
uint32_t reserved; // Must be zero (future use)
uint64_t magic; // kIntegrityFooterMagic
};
static_assert(sizeof(IntegrityFooter) == 16);
static_assert(std::has_unique_object_representations_v<IntegrityFooter>);
constexpr size_t kFooterSize = sizeof(IntegrityFooter);
bool IsSameFileObject(base::File* left, base::File* right) {
BY_HANDLE_FILE_INFORMATION left_info = {};
BY_HANDLE_FILE_INFORMATION right_info = {};
if (!::GetFileInformationByHandle(left->GetPlatformFile(), &left_info) ||
!::GetFileInformationByHandle(right->GetPlatformFile(), &right_info)) {
return true;
}
return left_info.dwVolumeSerialNumber == right_info.dwVolumeSerialNumber &&
left_info.nFileIndexHigh == right_info.nFileIndexHigh &&
left_info.nFileIndexLow == right_info.nFileIndexLow;
}
base::File OpenForConditionalDeleteNoFollow(const base::FilePath& path) {
return base::File(::CreateFileW(
path.value().c_str(), GENERIC_READ | DELETE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT, nullptr));
}
base::File OpenForReadNoFollow(const base::FilePath& path) {
return base::File(::CreateFileW(
path.value().c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT, nullptr));
}
bool IsOpenedReparsePoint(base::File* file) {
FILE_ATTRIBUTE_TAG_INFO info = {};
return !::GetFileInformationByHandleEx(file->GetPlatformFile(),
FileAttributeTagInfo, &info,
sizeof(info)) ||
(info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0;
}
std::optional<base::FilePath> GetOpenedResolvedPath(base::File* file) {
constexpr DWORD kMaxFinalPathLength = 32768;
DWORD capacity = MAX_PATH;
std::wstring buffer(capacity, L'\0');
DWORD length = ::GetFinalPathNameByHandleW(
file->GetPlatformFile(), buffer.data(), capacity,
FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);
if (length == 0 || length >= kMaxFinalPathLength) {
return std::nullopt;
}
if (length >= capacity) {
capacity = length + 1;
buffer.resize(capacity);
length = ::GetFinalPathNameByHandleW(
file->GetPlatformFile(), buffer.data(), capacity,
FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);
if (length == 0 || length >= capacity) {
return std::nullopt;
}
}
buffer.resize(length);
return base::FilePath(std::move(buffer));
}
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
base::NoDestructor<ConditionalDeleteHookForTesting> g_conditional_delete_hook;
#endif
ConditionalDeleteResult FinishConditionalDelete(base::File* file,
const base::FilePath& path) {
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
if (*g_conditional_delete_hook) {
g_conditional_delete_hook->Run(path);
}
#endif
if (file->DeleteOnClose(true)) {
return ConditionalDeleteResult::kDeleted;
}
// ReplaceFile may already have detached the observed file object from
// |path|. In that case the failed disposition cannot affect the replacement,
// and cleanup of the observed object is already complete for our purposes.
base::File current(path, base::File::FLAG_OPEN | base::File::FLAG_READ |
base::File::FLAG_WIN_SHARE_DELETE);
if (!current.IsValid()) {
return base::PathExists(path) ? ConditionalDeleteResult::kChanged
: ConditionalDeleteResult::kDeleted;
}
if (!IsSameFileObject(file, &current)) {
return ConditionalDeleteResult::kDeleted;
}
return ConditionalDeleteResult::kError;
}
} // namespace
bool WriteFileWithIntegrity(const base::FilePath& path,
const std::string& content) {
const base::FilePath parent = path.DirName();
if (!base::DirectoryExists(parent) || IsReparsePoint(parent) ||
IsReparsePoint(path)) {
return false;
}
EnsureCrcInitialized();
IntegrityFooter footer = {};
footer.data_crc32 = CrcCalc(content.data(), content.size());
footer.magic = kIntegrityFooterMagic;
std::string output;
output.reserve(content.size() + kFooterSize);
output.append(content);
output.append(reinterpret_cast<const char*>(&footer), kFooterSize);
base::FilePath temp_path;
if (!base::CreateTemporaryFileInDir(parent, &temp_path) ||
IsReparsePoint(temp_path)) {
return false;
}
bool write_succeeded = false;
{
base::File temp(temp_path, base::File::FLAG_OPEN | base::File::FLAG_WRITE |
base::File::FLAG_WIN_SHARE_DELETE);
if (temp.IsValid()) {
std::optional<size_t> written =
temp.WriteAtCurrentPos(base::as_byte_span(output));
write_succeeded = written && *written == output.size() && temp.Flush();
}
}
const bool replaced =
write_succeeded && base::ReplaceFile(temp_path, path, nullptr);
if (!replaced) {
base::DeleteFile(temp_path);
}
return replaced;
}
IntegrityResult ReadFileWithIntegrity(const base::FilePath& path,
std::string* content,
IntegrityMismatchAction mismatch_action,
size_t max_content_size,
base::FilePath* resolved_path,
std::string* raw_content,
bool* too_large) {
if (!content) {
return IntegrityResult::kReadError;
}
if (too_large) {
*too_large = false;
}
if (!base::PathExists(path)) {
return IntegrityResult::kFileNotFound;
}
std::string raw;
base::File file = OpenForReadNoFollow(path);
if (!file.IsValid() || IsOpenedReparsePoint(&file)) {
return IntegrityResult::kReadError;
}
if (resolved_path) {
std::optional<base::FilePath> opened_path = GetOpenedResolvedPath(&file);
if (!opened_path) {
return IntegrityResult::kReadError;
}
*resolved_path = std::move(*opened_path);
}
int64_t length = file.GetLength();
const size_t max_file_size =
max_content_size <= std::numeric_limits<size_t>::max() - kFooterSize
? max_content_size + kFooterSize
: std::numeric_limits<size_t>::max();
if (length < 0 || static_cast<uint64_t>(length) > max_file_size) {
if (too_large && length >= 0 &&
static_cast<uint64_t>(length) > max_file_size) {
*too_large = true;
}
return IntegrityResult::kReadError;
}
raw.resize(static_cast<size_t>(length));
std::optional<size_t> bytes_read =
file.ReadAtCurrentPos(base::as_writable_byte_span(raw));
if (!bytes_read || *bytes_read != raw.size()) {
return IntegrityResult::kReadError;
}
if (raw_content) {
*raw_content = raw;
}
// File too small to contain a footer.
if (raw.size() < kFooterSize) {
if (raw.size() > max_content_size) {
return IntegrityResult::kReadError;
}
*content = std::move(raw);
return IntegrityResult::kSuccessNoFooter;
}
// Check for magic number at end of file.
IntegrityFooter footer;
memcpy(&footer, raw.data() + raw.size() - kFooterSize, kFooterSize);
if (footer.magic != kIntegrityFooterMagic) {
if (raw.size() > max_content_size) {
return IntegrityResult::kReadError;
}
// No integrity footer — treat as legacy file.
*content = std::move(raw);
return IntegrityResult::kSuccessNoFooter;
}
// Footer present — verify CRC32 of content.
EnsureCrcInitialized();
size_t content_size = raw.size() - kFooterSize;
uint32_t actual_crc = CrcCalc(raw.data(), content_size);
if (actual_crc != footer.data_crc32) {
if (mismatch_action == IntegrityMismatchAction::kDoom) {
// Corrupted. Delete the file (doom pattern from Chromium's disk cache).
base::DeleteFile(path);
}
return IntegrityResult::kIntegrityMismatch;
}
// Integrity verified. Return content without footer.
raw.resize(content_size);
*content = std::move(raw);
return IntegrityResult::kSuccess;
}
std::optional<base::FilePath> GetSafeDirectoryResolvedPath(
const base::FilePath& path) {
base::File directory(::CreateFileW(
path.value().c_str(), FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
nullptr));
if (!directory.IsValid() || IsOpenedReparsePoint(&directory)) {
return std::nullopt;
}
FILE_ATTRIBUTE_TAG_INFO info = {};
if (!::GetFileInformationByHandleEx(directory.GetPlatformFile(),
FileAttributeTagInfo, &info,
sizeof(info)) ||
(info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) {
return std::nullopt;
}
return GetOpenedResolvedPath(&directory);
}
ConditionalDeleteResult DeleteFileWithIntegrityIfMatching(
const base::FilePath& path,
const std::string& expected_content,
bool expected_integrity_protected,
const base::FilePath& expected_resolved_parent) {
base::File file = OpenForConditionalDeleteNoFollow(path);
if (!file.IsValid()) {
return !base::PathExists(path) || IsReparsePoint(path)
? ConditionalDeleteResult::kChanged
: ConditionalDeleteResult::kError;
}
if (IsOpenedReparsePoint(&file)) {
return ConditionalDeleteResult::kChanged;
}
std::optional<base::FilePath> resolved_path = GetOpenedResolvedPath(&file);
if (!resolved_path || expected_resolved_parent.empty() ||
!base::FilePath::CompareEqualIgnoreCase(
resolved_path->DirName().value(), expected_resolved_parent.value())) {
return ConditionalDeleteResult::kChanged;
}
int64_t length = file.GetLength();
if (expected_content.size() > static_cast<size_t>(kMaxLaunchStateFileSize)) {
return ConditionalDeleteResult::kChanged;
}
const size_t expected_length =
expected_content.size() +
(expected_integrity_protected ? kFooterSize : 0u);
if (length < 0 || static_cast<uint64_t>(length) != expected_length) {
return ConditionalDeleteResult::kChanged;
}
if (static_cast<uint64_t>(length) > std::numeric_limits<size_t>::max()) {
return ConditionalDeleteResult::kError;
}
std::string raw(static_cast<size_t>(length), '\0');
std::optional<size_t> bytes_read =
file.ReadAtCurrentPos(base::as_writable_byte_span(raw));
if (!bytes_read || *bytes_read != raw.size()) {
return ConditionalDeleteResult::kError;
}
IntegrityResult integrity = IntegrityResult::kSuccessNoFooter;
std::string content;
if (raw.size() < kFooterSize) {
content = std::move(raw);
} else {
IntegrityFooter footer;
memcpy(&footer, raw.data() + raw.size() - kFooterSize, kFooterSize);
if (footer.magic != kIntegrityFooterMagic) {
content = std::move(raw);
} else {
EnsureCrcInitialized();
size_t content_size = raw.size() - kFooterSize;
if (CrcCalc(raw.data(), content_size) != footer.data_crc32) {
integrity = IntegrityResult::kIntegrityMismatch;
} else {
raw.resize(content_size);
content = std::move(raw);
integrity = IntegrityResult::kSuccess;
}
}
}
if (content != expected_content ||
((integrity == IntegrityResult::kSuccess) !=
expected_integrity_protected) ||
(integrity != IntegrityResult::kSuccess &&
integrity != IntegrityResult::kSuccessNoFooter)) {
return ConditionalDeleteResult::kChanged;
}
return FinishConditionalDelete(&file, path);
}
ConditionalDeleteResult DeleteFileRawIfMatching(
const base::FilePath& path,
const std::string& expected_raw_content,
const base::FilePath& expected_resolved_parent) {
base::File file = OpenForConditionalDeleteNoFollow(path);
if (!file.IsValid()) {
return !base::PathExists(path) || IsReparsePoint(path)
? ConditionalDeleteResult::kChanged
: ConditionalDeleteResult::kError;
}
if (IsOpenedReparsePoint(&file)) {
return ConditionalDeleteResult::kChanged;
}
std::optional<base::FilePath> resolved_path = GetOpenedResolvedPath(&file);
if (!resolved_path || expected_resolved_parent.empty() ||
!base::FilePath::CompareEqualIgnoreCase(
resolved_path->DirName().value(), expected_resolved_parent.value())) {
return ConditionalDeleteResult::kChanged;
}
if (expected_raw_content.size() >
static_cast<size_t>(kMaxLaunchStateFileSize) + kFooterSize) {
return ConditionalDeleteResult::kChanged;
}
const int64_t length = file.GetLength();
if (length < 0 ||
static_cast<uint64_t>(length) != expected_raw_content.size()) {
return ConditionalDeleteResult::kChanged;
}
std::string raw(static_cast<size_t>(length), '\0');
std::optional<size_t> bytes_read =
file.ReadAtCurrentPos(base::as_writable_byte_span(raw));
if (!bytes_read || *bytes_read != raw.size()) {
return ConditionalDeleteResult::kError;
}
if (raw != expected_raw_content) {
return ConditionalDeleteResult::kChanged;
}
return FinishConditionalDelete(&file, path);
}
ConditionalDeleteResult DeleteFileIfOversized(
const base::FilePath& path,
size_t max_content_size,
const base::FilePath& expected_resolved_parent) {
base::File file = OpenForConditionalDeleteNoFollow(path);
if (!file.IsValid()) {
return !base::PathExists(path) || IsReparsePoint(path)
? ConditionalDeleteResult::kChanged
: ConditionalDeleteResult::kError;
}
if (IsOpenedReparsePoint(&file)) {
return ConditionalDeleteResult::kChanged;
}
std::optional<base::FilePath> resolved_path = GetOpenedResolvedPath(&file);
if (!resolved_path || expected_resolved_parent.empty() ||
!base::FilePath::CompareEqualIgnoreCase(
resolved_path->DirName().value(), expected_resolved_parent.value())) {
return ConditionalDeleteResult::kChanged;
}
const size_t max_file_size =
max_content_size <= std::numeric_limits<size_t>::max() - kFooterSize
? max_content_size + kFooterSize
: std::numeric_limits<size_t>::max();
const int64_t length = file.GetLength();
if (length < 0) {
return ConditionalDeleteResult::kError;
}
if (static_cast<uint64_t>(length) <= max_file_size) {
return ConditionalDeleteResult::kChanged;
}
return FinishConditionalDelete(&file, path);
}
ConditionalDeleteResult DeleteFileIfReparsePoint(
const base::FilePath& path,
const base::FilePath& expected_resolved_parent) {
base::File file = OpenForConditionalDeleteNoFollow(path);
if (!file.IsValid()) {
return !base::PathExists(path) || !IsReparsePoint(path)
? ConditionalDeleteResult::kChanged
: ConditionalDeleteResult::kError;
}
FILE_ATTRIBUTE_TAG_INFO info = {};
if (!::GetFileInformationByHandleEx(
file.GetPlatformFile(), FileAttributeTagInfo, &info, sizeof(info))) {
return ConditionalDeleteResult::kError;
}
if ((info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) {
return ConditionalDeleteResult::kChanged;
}
std::optional<base::FilePath> resolved_path = GetOpenedResolvedPath(&file);
if (!resolved_path || expected_resolved_parent.empty() ||
!base::FilePath::CompareEqualIgnoreCase(
resolved_path->DirName().value(), expected_resolved_parent.value())) {
return ConditionalDeleteResult::kChanged;
}
return FinishConditionalDelete(&file, path);
}
void SetConditionalDeleteHookForTesting(
ConditionalDeleteHookForTesting callback) {
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
*g_conditional_delete_hook = std::move(callback);
#endif
}
const char* IntegrityResultToString(IntegrityResult result) {
switch (result) {
case IntegrityResult::kSuccess:
return "Success (verified)";
case IntegrityResult::kSuccessNoFooter:
return "Success (no integrity footer)";
case IntegrityResult::kFileNotFound:
return "File not found";
case IntegrityResult::kReadError:
return "Read error";
case IntegrityResult::kIntegrityMismatch:
return "Integrity mismatch";
}
return "Unknown";
}
} // namespace cef_installer
@@ -0,0 +1,126 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_FILE_INTEGRITY_H_
#define CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_FILE_INTEGRITY_H_
#include <limits>
#include <optional>
#include <string>
#include "base/files/file_path.h"
#include "base/functional/callback_forward.h"
namespace cef_installer {
// Result codes for integrity-checked file reads.
enum class IntegrityResult {
kSuccess, // Content read and CRC32 verified
kSuccessNoFooter, // Content read, no integrity footer (legacy file)
kFileNotFound, // File does not exist
kReadError, // Could not read file
kIntegrityMismatch, // CRC32 mismatch
};
enum class IntegrityMismatchAction {
kDoom,
kPreserve,
};
enum class ConditionalDeleteResult {
kDeleted,
kChanged,
kError,
};
// Atomically write content with a CRC32 integrity footer appended. The new
// contents are written to a unique same-directory temporary file and replaced
// into place only after the complete write succeeds. A failed write or replace
// leaves the prior destination intact when the platform supports it. No partial
// new destination is exposed to concurrent readers.
//
// On-disk format:
// [content bytes]
// [4 bytes: CRC32 of content, little-endian]
// [4 bytes: reserved (zero)]
// [8 bytes: magic number]
//
// Total footer overhead: 16 bytes.
// Returns true on success. Returns false for unsafe reparse paths, temporary
// creation/write failures, or replacement failures.
bool WriteFileWithIntegrity(const base::FilePath& path,
const std::string& content);
// Read a file, verifying its CRC32 integrity footer if present.
//
// - Footer present, CRC32 matches: returns kSuccess, |content| has the
// original data with footer stripped.
// - Footer present, CRC32 mismatch: returns kIntegrityMismatch and either
// deletes or preserves the corrupted file according to |mismatch_action|.
// - No footer (legacy file): returns kSuccessNoFooter, |content| has
// the full file contents.
//
// The doom-on-mismatch behavior follows Chromium's disk cache pattern:
// a corrupted entry is worse than a missing one.
IntegrityResult ReadFileWithIntegrity(
const base::FilePath& path,
std::string* content,
IntegrityMismatchAction mismatch_action = IntegrityMismatchAction::kDoom,
size_t max_content_size = std::numeric_limits<size_t>::max(),
base::FilePath* resolved_path = nullptr,
std::string* raw_content = nullptr,
bool* too_large = nullptr);
// Opens |path| without following a reparse point in the final component,
// rejects a reparse-point directory, and returns the handle-resolved path.
std::optional<base::FilePath> GetSafeDirectoryResolvedPath(
const base::FilePath& path);
// Opens |path| itself without following a reparse point, verifies that the
// handle-resolved parent is |expected_resolved_parent|, verifies the payload
// and footer status on that handle, and marks that same file object for
// deletion only on an exact match. A concurrent atomic replacement names a
// different file object and is therefore preserved.
ConditionalDeleteResult DeleteFileWithIntegrityIfMatching(
const base::FilePath& path,
const std::string& expected_content,
bool expected_integrity_protected,
const base::FilePath& expected_resolved_parent);
// Opens |path| without following a reparse point, verifies its resolved parent
// and complete raw bytes, and marks that same file object for deletion only on
// an exact match. This is used for conclusively invalid publications whose
// integrity payload cannot be represented by the structured helper above.
ConditionalDeleteResult DeleteFileRawIfMatching(
const base::FilePath& path,
const std::string& expected_raw_content,
const base::FilePath& expected_resolved_parent);
// Marks the same no-follow file object for deletion only if it still exceeds
// |max_content_size| plus the maximum integrity footer and its resolved parent
// matches. A bounded valid replacement is preserved.
ConditionalDeleteResult DeleteFileIfOversized(
const base::FilePath& path,
size_t max_content_size,
const base::FilePath& expected_resolved_parent);
// Opens |path| without following the final component and marks that same file
// object for deletion only if it is still a reparse point whose resolved
// parent matches |expected_resolved_parent|. The reparse target is never
// opened, and a concurrent replacement is preserved.
ConditionalDeleteResult DeleteFileIfReparsePoint(
const base::FilePath& path,
const base::FilePath& expected_resolved_parent);
using ConditionalDeleteHookForTesting =
base::RepeatingCallback<void(const base::FilePath&)>;
void SetConditionalDeleteHookForTesting(
ConditionalDeleteHookForTesting callback);
// Convert result to string for logging.
const char* IntegrityResultToString(IntegrityResult result);
} // namespace cef_installer
#endif // CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_FILE_INTEGRITY_H_
@@ -0,0 +1,452 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cef/libcef_dll/bootstrap/installer/installer_file_integrity.h"
#include <windows.h>
#include <atomic>
#include <thread>
#include <vector>
#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "cef/libcef_dll/bootstrap/installer/installer_constants.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace cef_installer {
namespace {
class InstallerFileIntegrityTest : public testing::Test {
protected:
void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); }
void TearDown() override { SetConditionalDeleteHookForTesting({}); }
base::FilePath TestPath(const std::string& name) {
return temp_dir_.GetPath().AppendASCII(name);
}
base::FilePath ResolvedParent(const base::FilePath& path) {
std::optional<base::FilePath> resolved =
GetSafeDirectoryResolvedPath(path.DirName());
EXPECT_TRUE(resolved);
return resolved.value_or(base::FilePath());
}
base::ScopedTempDir temp_dir_;
};
// ============================================================================
// WriteFileWithIntegrity Tests
// ============================================================================
TEST_F(InstallerFileIntegrityTest, WriteCreatesFile) {
base::FilePath path = TestPath("test.dat");
EXPECT_TRUE(WriteFileWithIntegrity(path, "hello"));
EXPECT_TRUE(base::PathExists(path));
// File should be content + 16-byte footer.
std::optional<int64_t> size = base::GetFileSize(path);
ASSERT_TRUE(size.has_value());
EXPECT_EQ(5 + 16, size.value());
}
TEST_F(InstallerFileIntegrityTest, WriteEmptyContent) {
base::FilePath path = TestPath("empty.dat");
EXPECT_TRUE(WriteFileWithIntegrity(path, ""));
EXPECT_TRUE(base::PathExists(path));
// File should be just the 16-byte footer.
std::optional<int64_t> size = base::GetFileSize(path);
ASSERT_TRUE(size.has_value());
EXPECT_EQ(16, size.value());
}
TEST_F(InstallerFileIntegrityTest, OverwriteIsAtomicRoundTrip) {
base::FilePath path = TestPath("overwrite.dat");
ASSERT_TRUE(WriteFileWithIntegrity(path, "old"));
ASSERT_TRUE(WriteFileWithIntegrity(path, "new"));
std::string content;
EXPECT_EQ(IntegrityResult::kSuccess, ReadFileWithIntegrity(path, &content));
EXPECT_EQ("new", content);
}
TEST_F(InstallerFileIntegrityTest, ReplaceFailurePreservesPriorFile) {
base::FilePath path = TestPath("locked.dat");
ASSERT_TRUE(WriteFileWithIntegrity(path, "prior"));
{
base::File lock(path, base::File::FLAG_OPEN | base::File::FLAG_READ);
ASSERT_TRUE(lock.IsValid());
EXPECT_FALSE(WriteFileWithIntegrity(path, "replacement"));
}
std::string content;
EXPECT_EQ(IntegrityResult::kSuccess, ReadFileWithIntegrity(path, &content));
EXPECT_EQ("prior", content);
}
TEST_F(InstallerFileIntegrityTest, ConcurrentWritersPublishWholeFile) {
base::FilePath path = TestPath("concurrent.dat");
std::vector<std::string> payloads;
std::vector<std::thread> writers;
std::atomic<int> successes = 0;
for (int i = 0; i < 8; ++i) {
payloads.push_back(std::string(4096, static_cast<char>('A' + i)));
}
for (const auto& payload : payloads) {
writers.emplace_back([&, payload] {
if (WriteFileWithIntegrity(path, payload)) {
++successes;
}
});
}
for (auto& writer : writers) {
writer.join();
}
EXPECT_GT(successes.load(), 0);
std::string content;
ASSERT_EQ(IntegrityResult::kSuccess, ReadFileWithIntegrity(path, &content));
EXPECT_NE(payloads.end(),
std::find(payloads.begin(), payloads.end(), content));
}
// ============================================================================
// ReadFileWithIntegrity Tests
// ============================================================================
TEST_F(InstallerFileIntegrityTest, RoundTrip) {
base::FilePath path = TestPath("roundtrip.dat");
std::string original = "Hello, CEF Installer!";
ASSERT_TRUE(WriteFileWithIntegrity(path, original));
std::string content;
IntegrityResult result = ReadFileWithIntegrity(path, &content);
EXPECT_EQ(IntegrityResult::kSuccess, result);
EXPECT_EQ(original, content);
}
TEST_F(InstallerFileIntegrityTest, RoundTripEmptyContent) {
base::FilePath path = TestPath("empty_roundtrip.dat");
ASSERT_TRUE(WriteFileWithIntegrity(path, ""));
std::string content;
IntegrityResult result = ReadFileWithIntegrity(path, &content);
EXPECT_EQ(IntegrityResult::kSuccess, result);
EXPECT_TRUE(content.empty());
}
TEST_F(InstallerFileIntegrityTest, RoundTripLargeContent) {
base::FilePath path = TestPath("large.dat");
std::string original(100 * 1024, 'X'); // 100 KB
ASSERT_TRUE(WriteFileWithIntegrity(path, original));
std::string content;
IntegrityResult result = ReadFileWithIntegrity(path, &content);
EXPECT_EQ(IntegrityResult::kSuccess, result);
EXPECT_EQ(original, content);
}
TEST_F(InstallerFileIntegrityTest, RoundTripJsonContent) {
base::FilePath path = TestPath("manifest.cache");
std::string json = R"({"version":"137.3.5","file":"cef_137.3.5.tar.xz"})";
ASSERT_TRUE(WriteFileWithIntegrity(path, json));
std::string content;
IntegrityResult result = ReadFileWithIntegrity(path, &content);
EXPECT_EQ(IntegrityResult::kSuccess, result);
EXPECT_EQ(json, content);
}
TEST_F(InstallerFileIntegrityTest, RoundTripBinaryContent) {
base::FilePath path = TestPath("binary.dat");
// Content with null bytes and non-printable characters.
std::string original = std::string("\x00\x01\x02\xff\xfe\xfd", 6);
ASSERT_TRUE(WriteFileWithIntegrity(path, original));
std::string content;
IntegrityResult result = ReadFileWithIntegrity(path, &content);
EXPECT_EQ(IntegrityResult::kSuccess, result);
EXPECT_EQ(original, content);
}
TEST_F(InstallerFileIntegrityTest, ReadNonExistentFile) {
base::FilePath path = TestPath("nonexistent.dat");
std::string content;
IntegrityResult result = ReadFileWithIntegrity(path, &content);
EXPECT_EQ(IntegrityResult::kFileNotFound, result);
}
TEST_F(InstallerFileIntegrityTest, ReadLegacyFileNoFooter) {
base::FilePath path = TestPath("legacy.dat");
std::string original = "legacy content without footer";
ASSERT_TRUE(base::WriteFile(path, original));
std::string content;
IntegrityResult result = ReadFileWithIntegrity(path, &content);
EXPECT_EQ(IntegrityResult::kSuccessNoFooter, result);
EXPECT_EQ(original, content);
}
TEST_F(InstallerFileIntegrityTest, ReadSmallLegacyFile) {
base::FilePath path = TestPath("tiny.dat");
// File smaller than 16-byte footer — always treated as legacy.
std::string original = "tiny";
ASSERT_TRUE(base::WriteFile(path, original));
std::string content;
IntegrityResult result = ReadFileWithIntegrity(path, &content);
EXPECT_EQ(IntegrityResult::kSuccessNoFooter, result);
EXPECT_EQ(original, content);
}
TEST_F(InstallerFileIntegrityTest, ReadEmptyFile) {
base::FilePath path = TestPath("empty.dat");
ASSERT_TRUE(base::WriteFile(path, ""));
std::string content;
IntegrityResult result = ReadFileWithIntegrity(path, &content);
EXPECT_EQ(IntegrityResult::kSuccessNoFooter, result);
EXPECT_TRUE(content.empty());
}
// ============================================================================
// Corruption Detection (Doom Pattern)
// ============================================================================
TEST_F(InstallerFileIntegrityTest, CorruptedContentDetected) {
base::FilePath path = TestPath("corrupt.dat");
ASSERT_TRUE(WriteFileWithIntegrity(path, "original content"));
ASSERT_TRUE(base::PathExists(path));
// Corrupt the content by overwriting the first byte.
std::string raw;
ASSERT_TRUE(base::ReadFileToString(path, &raw));
raw[0] = ~raw[0]; // Flip bits in first byte
ASSERT_TRUE(base::WriteFile(path, raw));
std::string content;
IntegrityResult result = ReadFileWithIntegrity(path, &content);
EXPECT_EQ(IntegrityResult::kIntegrityMismatch, result);
// File should be doomed (deleted).
EXPECT_FALSE(base::PathExists(path));
}
TEST_F(InstallerFileIntegrityTest, CorruptedCrcDetected) {
base::FilePath path = TestPath("bad_crc.dat");
ASSERT_TRUE(WriteFileWithIntegrity(path, "test data"));
// Corrupt the CRC32 field (bytes at offset content_size).
std::string raw;
ASSERT_TRUE(base::ReadFileToString(path, &raw));
size_t crc_offset = raw.size() - 16; // CRC32 is first 4 bytes of footer
raw[crc_offset] = ~raw[crc_offset];
ASSERT_TRUE(base::WriteFile(path, raw));
std::string content;
IntegrityResult result = ReadFileWithIntegrity(path, &content);
EXPECT_EQ(IntegrityResult::kIntegrityMismatch, result);
EXPECT_FALSE(base::PathExists(path));
}
TEST_F(InstallerFileIntegrityTest, CorruptedCrcCanBePreserved) {
base::FilePath path = TestPath("preserved_bad_crc.dat");
ASSERT_TRUE(WriteFileWithIntegrity(path, "test data"));
std::string raw;
ASSERT_TRUE(base::ReadFileToString(path, &raw));
raw[0] = ~raw[0];
ASSERT_TRUE(base::WriteFile(path, raw));
std::string content;
EXPECT_EQ(IntegrityResult::kIntegrityMismatch,
ReadFileWithIntegrity(path, &content,
IntegrityMismatchAction::kPreserve));
std::string unchanged;
ASSERT_TRUE(base::ReadFileToString(path, &unchanged));
EXPECT_EQ(raw, unchanged);
}
TEST_F(InstallerFileIntegrityTest,
ConditionalDeletePreservesAtomicReplacementAfterComparison) {
base::FilePath path = TestPath("conditional_delete.dat");
ASSERT_TRUE(WriteFileWithIntegrity(path, "old"));
SetConditionalDeleteHookForTesting(
base::BindRepeating([](const base::FilePath& replacement_path) {
ASSERT_TRUE(WriteFileWithIntegrity(replacement_path, "new"));
}));
EXPECT_EQ(ConditionalDeleteResult::kDeleted,
DeleteFileWithIntegrityIfMatching(path, "old", true,
ResolvedParent(path)));
std::string content;
EXPECT_EQ(IntegrityResult::kSuccess, ReadFileWithIntegrity(path, &content));
EXPECT_EQ("new", content);
}
TEST_F(InstallerFileIntegrityTest, ConditionalDeletePreservesChangedPayload) {
base::FilePath path = TestPath("conditional_changed.dat");
ASSERT_TRUE(WriteFileWithIntegrity(path, "new"));
EXPECT_EQ(ConditionalDeleteResult::kChanged,
DeleteFileWithIntegrityIfMatching(path, "old", true,
ResolvedParent(path)));
EXPECT_TRUE(base::PathExists(path));
}
TEST_F(InstallerFileIntegrityTest,
ConditionalDeleteRejectsUnexpectedLengthBeforeReading) {
base::FilePath path = TestPath("conditional_oversized.dat");
ASSERT_TRUE(
base::WriteFile(path, std::string(kMaxLaunchStateFileSize + 1024, 'x')));
EXPECT_EQ(ConditionalDeleteResult::kChanged,
DeleteFileWithIntegrityIfMatching(path, "old", true,
ResolvedParent(path)));
EXPECT_TRUE(base::PathExists(path));
}
TEST_F(InstallerFileIntegrityTest, ConditionalDeleteDoesNotFollowReparsePoint) {
base::FilePath target = TestPath("conditional_target.dat");
base::FilePath link = TestPath("conditional_link.dat");
ASSERT_TRUE(WriteFileWithIntegrity(target, "old"));
constexpr DWORD kAllowUnprivilegedCreate = 0x2;
if (!::CreateSymbolicLinkW(link.value().c_str(), target.value().c_str(),
kAllowUnprivilegedCreate)) {
GTEST_SKIP() << "Symbolic-link creation is unavailable";
}
EXPECT_EQ(ConditionalDeleteResult::kChanged,
DeleteFileWithIntegrityIfMatching(link, "old", true,
ResolvedParent(link)));
EXPECT_TRUE(base::PathExists(target));
}
TEST_F(InstallerFileIntegrityTest,
ConditionalReparseDeleteRemovesLinkAndPreservesTarget) {
base::FilePath target = TestPath("reparse_delete_target.dat");
base::FilePath link = TestPath("reparse_delete_link.dat");
ASSERT_TRUE(WriteFileWithIntegrity(target, "target"));
constexpr DWORD kAllowUnprivilegedCreate = 0x2;
if (!::CreateSymbolicLinkW(link.value().c_str(), target.value().c_str(),
kAllowUnprivilegedCreate)) {
GTEST_SKIP() << "Symbolic-link creation is unavailable";
}
EXPECT_EQ(ConditionalDeleteResult::kDeleted,
DeleteFileIfReparsePoint(link, ResolvedParent(link)));
EXPECT_FALSE(base::PathExists(link));
std::string content;
EXPECT_EQ(IntegrityResult::kSuccess, ReadFileWithIntegrity(target, &content));
EXPECT_EQ("target", content);
}
TEST_F(InstallerFileIntegrityTest,
ConditionalReparseDeletePreservesConcurrentReplacement) {
base::FilePath target = TestPath("reparse_race_target.dat");
base::FilePath link = TestPath("reparse_race_link.dat");
ASSERT_TRUE(WriteFileWithIntegrity(target, "target"));
constexpr DWORD kAllowUnprivilegedCreate = 0x2;
if (!::CreateSymbolicLinkW(link.value().c_str(), target.value().c_str(),
kAllowUnprivilegedCreate)) {
GTEST_SKIP() << "Symbolic-link creation is unavailable";
}
SetConditionalDeleteHookForTesting(
base::BindRepeating([](const base::FilePath& hook_path) {
ASSERT_TRUE(base::DeleteFile(hook_path));
ASSERT_TRUE(WriteFileWithIntegrity(hook_path, "replacement"));
}));
EXPECT_EQ(ConditionalDeleteResult::kDeleted,
DeleteFileIfReparsePoint(link, ResolvedParent(link)));
std::string content;
EXPECT_EQ(IntegrityResult::kSuccess, ReadFileWithIntegrity(link, &content));
EXPECT_EQ("replacement", content);
EXPECT_TRUE(base::PathExists(target));
}
TEST_F(InstallerFileIntegrityTest,
ConditionalDeleteRejectsRedirectedParentDirectory) {
base::FilePath parent = TestPath("launch");
base::FilePath target = TestPath("redirect_target");
base::FilePath path = parent.Append(L"evidence");
ASSERT_TRUE(base::CreateDirectory(parent));
ASSERT_TRUE(WriteFileWithIntegrity(path, "old"));
std::optional<base::FilePath> expected_parent =
GetSafeDirectoryResolvedPath(parent);
ASSERT_TRUE(expected_parent);
ASSERT_TRUE(base::DeletePathRecursively(parent));
ASSERT_TRUE(base::CreateDirectory(target));
base::FilePath redirected_file = target.Append(L"evidence");
ASSERT_TRUE(WriteFileWithIntegrity(redirected_file, "old"));
constexpr DWORD kDirectorySymlinkWithUnprivilegedCreate = 0x3;
if (!::CreateSymbolicLinkW(parent.value().c_str(), target.value().c_str(),
kDirectorySymlinkWithUnprivilegedCreate)) {
GTEST_SKIP() << "Directory symbolic-link creation is unavailable";
}
EXPECT_EQ(
ConditionalDeleteResult::kChanged,
DeleteFileWithIntegrityIfMatching(path, "old", true, *expected_parent));
EXPECT_TRUE(base::PathExists(redirected_file));
EXPECT_TRUE(::RemoveDirectoryW(parent.value().c_str()));
}
TEST_F(InstallerFileIntegrityTest, TruncatedFileWithFooterMagic) {
// A file that has exactly 16 bytes (just a footer, no content).
// If the magic matches, it should verify the CRC of empty content.
base::FilePath path = TestPath("footer_only.dat");
ASSERT_TRUE(WriteFileWithIntegrity(path, ""));
std::string content;
IntegrityResult result = ReadFileWithIntegrity(path, &content);
EXPECT_EQ(IntegrityResult::kSuccess, result);
EXPECT_TRUE(content.empty());
}
// ============================================================================
// IntegrityResultToString Tests
// ============================================================================
TEST_F(InstallerFileIntegrityTest, ResultToString) {
EXPECT_STREQ("Success (verified)",
IntegrityResultToString(IntegrityResult::kSuccess));
EXPECT_STREQ("Success (no integrity footer)",
IntegrityResultToString(IntegrityResult::kSuccessNoFooter));
EXPECT_STREQ("File not found",
IntegrityResultToString(IntegrityResult::kFileNotFound));
EXPECT_STREQ("Read error",
IntegrityResultToString(IntegrityResult::kReadError));
EXPECT_STREQ("Integrity mismatch",
IntegrityResultToString(IntegrityResult::kIntegrityMismatch));
}
} // namespace
} // namespace cef_installer
@@ -0,0 +1,566 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cef/libcef_dll/bootstrap/installer/installer_file_ops.h"
#include <windows.h>
#include <utility>
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/rand_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "cef/libcef_dll/bootstrap/installer/installer_constants.h"
#include "cef/libcef_dll/bootstrap/installer/installer_e2e_config.h"
#include "cef/libcef_dll/bootstrap/installer/installer_paths.h"
namespace cef_installer {
namespace {
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
FileOpsFault g_file_ops_fault = FileOpsFault::kNone;
#endif
FileOpsFault EffectiveFileOpsFault() {
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
if (g_file_ops_fault != FileOpsFault::kNone) {
return g_file_ops_fault;
}
switch (internal::GetInstallerE2EConfig().file_ops_fault) {
case internal::InstallerE2EFileOpsFault::kNone:
break;
case internal::InstallerE2EFileOpsFault::kQuarantineMove:
return FileOpsFault::kQuarantineMove;
case internal::InstallerE2EFileOpsFault::kRepairMove:
return FileOpsFault::kRepairMove;
case internal::InstallerE2EFileOpsFault::kTrashMove:
return FileOpsFault::kTrashMove;
case internal::InstallerE2EFileOpsFault::kTrashReclaim:
return FileOpsFault::kTrashReclaim;
}
#endif
return FileOpsFault::kNone;
}
// Validate that a directory path is safe for file operations.
// Returns false if the path is a reparse point (symlink/junction).
bool IsDirectorySafe(const base::FilePath& dir_path) {
if (!base::DirectoryExists(dir_path)) {
return false;
}
return !IsReparsePoint(dir_path);
}
// Generate a unique trash entry path under trash_root.
base::FilePath GenerateTrashPath(const base::FilePath& trash_root,
const Version& version) {
// Format: <version>_<random>
uint64_t random_value = base::RandUint64();
std::string random_suffix = base::HexEncode(base::span<const uint8_t>(
reinterpret_cast<const uint8_t*>(&random_value), sizeof(random_value)));
std::string entry_name = version.ToString() + "_" + random_suffix;
return trash_root.Append(base::FilePath::FromUTF8Unsafe(entry_name));
}
// Map Windows error code to FileOpsError
FileOpsError MapWindowsError(DWORD error) {
switch (error) {
case ERROR_SUCCESS:
return FileOpsError::kSuccess;
case ERROR_FILE_NOT_FOUND:
case ERROR_PATH_NOT_FOUND:
return FileOpsError::kSourceNotFound;
case ERROR_ALREADY_EXISTS:
case ERROR_FILE_EXISTS:
return FileOpsError::kDestinationExists;
case ERROR_ACCESS_DENIED:
return FileOpsError::kAccessDenied;
case ERROR_DISK_FULL:
case ERROR_HANDLE_DISK_FULL:
return FileOpsError::kDiskFull;
case ERROR_SHARING_VIOLATION:
case ERROR_LOCK_VIOLATION:
return FileOpsError::kInUse;
default:
return FileOpsError::kRenameFailed;
}
}
// Delete a file or directory without ever enumerating a reparse point. A
// partially deleted directory remains retryable when an entry is in use.
bool TryDeletePath(const base::FilePath& path) {
DWORD attributes = ::GetFileAttributesW(path.value().c_str());
if (attributes == INVALID_FILE_ATTRIBUTES) {
DWORD error = ::GetLastError();
return error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND;
}
if (attributes & FILE_ATTRIBUTE_READONLY) {
if (!::SetFileAttributesW(path.value().c_str(),
attributes & ~FILE_ATTRIBUTE_READONLY)) {
return false;
}
attributes &= ~FILE_ATTRIBUTE_READONLY;
}
if (attributes & FILE_ATTRIBUTE_REPARSE_POINT) {
return attributes & FILE_ATTRIBUTE_DIRECTORY
? !!::RemoveDirectoryW(path.value().c_str())
: !!::DeleteFileW(path.value().c_str());
}
if (!(attributes & FILE_ATTRIBUTE_DIRECTORY)) {
return !!::DeleteFileW(path.value().c_str());
}
base::FileEnumerator enumerator(
path, false,
base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES,
base::FilePath::StringType(),
base::FileEnumerator::FolderSearchPolicy::MATCH_ONLY,
base::FileEnumerator::ErrorPolicy::STOP_ENUMERATION);
bool all_deleted = true;
for (base::FilePath child = enumerator.Next(); !child.empty();
child = enumerator.Next()) {
if (!TryDeletePath(child)) {
all_deleted = false;
}
}
if (enumerator.GetError() != base::File::FILE_OK) {
all_deleted = false;
}
if (!all_deleted) {
return false;
}
return !!::RemoveDirectoryW(path.value().c_str());
}
bool IsSafeCollisionTarget(const base::FilePath& install_dir,
const base::FilePath& target) {
if (!IsPathSafeForLoading(install_dir, target)) {
return false;
}
DWORD attributes = ::GetFileAttributesW(target.value().c_str());
if (attributes == INVALID_FILE_ATTRIBUTES ||
(attributes & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DEVICE))) {
return false;
}
return true;
}
} // namespace
void SetFileOpsFaultForTesting(FileOpsFault fault) {
#if defined(OFFICIAL_BUILD) && defined(NDEBUG)
(void)fault;
#else
g_file_ops_fault = fault;
#endif
}
VersionLease::VersionLease(HANDLE handle) : handle_(handle) {}
VersionLease::~VersionLease() {
if (IsValid()) {
CloseHandle(handle_);
}
}
VersionLease::VersionLease(VersionLease&& other) noexcept
: handle_(std::exchange(other.handle_, INVALID_HANDLE_VALUE)) {}
VersionLease& VersionLease::operator=(VersionLease&& other) noexcept {
if (this != &other) {
if (IsValid()) {
CloseHandle(handle_);
}
handle_ = std::exchange(other.handle_, INVALID_HANDLE_VALUE);
}
return *this;
}
bool VersionLease::IsValid() const {
return handle_ != INVALID_HANDLE_VALUE;
}
VersionLeaseError AcquireVersionLease(const base::FilePath& trusted_root,
const base::FilePath& version_dir,
std::unique_ptr<VersionLease>* lease) {
if (!lease) {
return VersionLeaseError::kInvalidPath;
}
lease->reset();
base::FilePath libcef_path = GetLibcefPath(version_dir);
if (!IsPathSafeForLoading(trusted_root, libcef_path)) {
if (!base::PathExists(version_dir) || !base::PathExists(libcef_path)) {
return VersionLeaseError::kLostRace;
}
return VersionLeaseError::kInvalidPath;
}
HANDLE handle = CreateFileW(libcef_path.value().c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (handle == INVALID_HANDLE_VALUE) {
DWORD error = GetLastError();
if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) {
return VersionLeaseError::kLostRace;
}
return VersionLeaseError::kOpenFailed;
}
*lease = std::unique_ptr<VersionLease>(new VersionLease(handle));
return VersionLeaseError::kSuccess;
}
FileOpsError InstallVersion(const base::FilePath& source_dir,
const base::FilePath& install_dir,
const Version& version,
bool* cleanup_deferred) {
if (cleanup_deferred) {
*cleanup_deferred = false;
}
FileOpsFault fault = EffectiveFileOpsFault();
// Validate inputs
if (!version.IsValid()) {
return FileOpsError::kSourceNotFound;
}
// 1. Verify source exists and is not a reparse point (symlink/junction)
// This prevents attacks where an attacker replaces the temp directory with
// a junction pointing to sensitive files.
if (!IsDirectorySafe(source_dir)) {
return FileOpsError::kSourceNotFound;
}
// 2. Construct destination path
base::FilePath dest_dir = GetVersionPath(install_dir, version);
// 3. Check existing ancestors before any collision mutation.
base::FilePath parent_dir = dest_dir.DirName();
for (base::FilePath dir = parent_dir;
dir != install_dir && dir != dir.DirName(); dir = dir.DirName()) {
if (base::PathExists(dir) && IsReparsePoint(dir)) {
return FileOpsError::kAccessDenied;
}
}
base::FilePath quarantined;
// 4. Quarantine every safe existing target opaquely. Destination contents
// are never read or classified; verified staging always wins the collision.
if (base::PathExists(dest_dir)) {
if (!IsSafeCollisionTarget(install_dir, dest_dir)) {
return FileOpsError::kQuarantineFailed;
}
base::FilePath trash_root = install_dir.Append(kTrashSubdirectory);
if (!VerifySafeDirectoryPath(trash_root) ||
(!base::DirectoryExists(trash_root) &&
!base::CreateDirectory(trash_root))) {
return FileOpsError::kQuarantineFailed;
}
quarantined = GenerateTrashPath(trash_root, version);
if (fault == FileOpsFault::kQuarantineMove ||
!::MoveFileExW(dest_dir.value().c_str(), quarantined.value().c_str(),
MOVEFILE_WRITE_THROUGH)) {
return FileOpsError::kQuarantineFailed;
}
}
// 5. Create parent directories if needed.
if (!base::DirectoryExists(parent_dir) &&
!base::CreateDirectory(parent_dir)) {
return FileOpsError::kAccessDenied;
}
// 6. Perform atomic rename
// On Windows, MoveFileEx with MOVEFILE_WRITE_THROUGH ensures durability.
// The caller is expected to place source_dir on the same volume as
// install_dir (e.g., via the .staging/ subdirectory) so this always
// succeeds as an atomic rename.
if (fault == FileOpsFault::kRepairMove && !quarantined.empty()) {
return FileOpsError::kRepairFailed;
}
if (!::MoveFileExW(source_dir.value().c_str(), dest_dir.value().c_str(),
MOVEFILE_WRITE_THROUGH)) {
if (!quarantined.empty()) {
return FileOpsError::kRepairFailed;
}
return MapWindowsError(::GetLastError());
}
// 7. Reclaim the quarantined path best-effort without traversing reparse
// points. Publication is already logically complete at this boundary.
if (!quarantined.empty() &&
(fault == FileOpsFault::kTrashReclaim || !TryDeletePath(quarantined)) &&
cleanup_deferred) {
*cleanup_deferred = true;
}
return FileOpsError::kSuccess;
}
FileOpsError UninstallVersion(const base::FilePath& install_dir,
const Version& version,
const std::string& platform) {
// Validate inputs
if (!version.IsValid() || platform.empty()) {
return FileOpsError::kDestinationNotFound;
}
FileOpsFault fault = EffectiveFileOpsFault();
// 1. Verify source exists and is not a reparse point.
base::FilePath source_dir = GetVersionPath(install_dir, version, platform);
if (!IsDirectorySafe(source_dir)) {
return FileOpsError::kDestinationNotFound;
}
// 2. Check intermediate directories for reparse points
// (walk from source_dir up to install_dir).
for (base::FilePath dir = source_dir.DirName();
dir != install_dir && dir != dir.DirName(); dir = dir.DirName()) {
if (IsReparsePoint(dir)) {
return FileOpsError::kDestinationNotFound;
}
}
// 3. Create .trash/ directory if needed, checking for reparse points.
base::FilePath trash_root = install_dir.Append(kTrashSubdirectory);
if (!VerifySafeDirectoryPath(trash_root)) {
return FileOpsError::kAccessDenied;
}
if (!base::DirectoryExists(trash_root) &&
!base::CreateDirectory(trash_root)) {
return FileOpsError::kAccessDenied;
}
// 4. Generate trash entry path and atomic rename
base::FilePath trash_dir = GenerateTrashPath(trash_root, version);
if (fault == FileOpsFault::kTrashMove) {
return FileOpsError::kRenameFailed;
}
if (!::MoveFileExW(source_dir.value().c_str(), trash_dir.value().c_str(),
MOVEFILE_WRITE_THROUGH)) {
return MapWindowsError(::GetLastError());
}
// 5. Try to delete trash directory
FileOpsError result = FileOpsError::kSuccess;
if (fault == FileOpsFault::kTrashReclaim || !TryDeletePath(trash_dir)) {
// Files are in use - leave for retry on next run
// This is not a failure; the version is effectively uninstalled
// (moved to trash) and will be cleaned up later
result = FileOpsError::kInUse;
}
// 6. Try to remove empty parent version directory (e.g., Versions/137.3.5/)
// This cleans up after the last platform for a version is uninstalled.
// DeleteFile on a directory only succeeds if empty, so this is safe.
base::FilePath version_parent = source_dir.DirName();
base::DeleteFile(version_parent); // Ignore result - fails if not empty
return result;
}
FileOpsError UninstallVersion(const base::FilePath& install_dir,
const Version& version) {
return UninstallVersion(install_dir, version, GetCurrentPlatform());
}
int RetryPendingDeletions(const base::FilePath& install_dir) {
int deleted_count = 0;
base::FilePath trash_root = install_dir.Append(kTrashSubdirectory);
if (!base::DirectoryExists(trash_root) || IsReparsePoint(trash_root)) {
return 0;
}
// Scan entries in .trash/ and try to delete them
base::FileEnumerator enumerator(
trash_root, false,
base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES);
for (base::FilePath path = enumerator.Next(); !path.empty();
path = enumerator.Next()) {
// TryDeletePath removes reparse points themselves and never enumerates
// their targets.
if (TryDeletePath(path)) {
deleted_count++;
}
}
return deleted_count;
}
bool IsReparsePoint(const base::FilePath& path) {
DWORD attrs = ::GetFileAttributesW(path.value().c_str());
if (attrs == INVALID_FILE_ATTRIBUTES) {
return false;
}
return (attrs & FILE_ATTRIBUTE_REPARSE_POINT) != 0;
}
bool VerifySafeFilePath(const base::FilePath& path, bool delete_if_exists) {
if (IsReparsePoint(path)) {
if (!base::DeleteFile(path)) {
return false;
}
}
if (delete_if_exists && base::PathExists(path)) {
base::DeleteFile(path);
}
return true;
}
bool VerifySafeDirectoryPath(const base::FilePath& path,
bool delete_if_exists) {
if (IsReparsePoint(path)) {
if (!base::DeleteFile(path)) {
return false;
}
}
if (delete_if_exists && base::DirectoryExists(path)) {
base::DeletePathRecursively(path);
}
return true;
}
bool IsReadableDirectory(const base::FilePath& path) {
if (path.empty()) {
return false;
}
if (!base::DirectoryExists(path)) {
return false;
}
// Security: Reject reparse points (symlinks/junctions).
return !IsReparsePoint(path);
}
bool IsPathSafeForLoading(const base::FilePath& trusted_root,
const base::FilePath& path,
base::TimeTicks deadline,
bool* time_limit_reached) {
auto time_expired = [&]() {
if (!deadline.is_null() && base::TimeTicks::Now() >= deadline) {
if (time_limit_reached) {
*time_limit_reached = true;
}
return true;
}
return false;
};
if (time_limit_reached) {
*time_limit_reached = false;
}
if (time_expired()) {
return false;
}
if (trusted_root.empty() || path.empty()) {
return false;
}
if (!trusted_root.IsAbsolute() || !path.IsAbsolute()) {
return false;
}
if (!trusted_root.IsParent(path)) {
return false;
}
// Collect components between root (exclusive) and path (inclusive) by
// walking up from path to root.
std::vector<base::FilePath> to_check;
for (base::FilePath current = path; current != trusted_root;
current = current.DirName()) {
if (current == current.DirName()) {
return false;
}
to_check.push_back(current);
}
// Check top-down (reverse order) so we fail on the shallowest
// reparse point first.
for (auto it = to_check.rbegin(); it != to_check.rend(); ++it) {
if (time_expired() || !base::PathExists(*it) || time_expired()) {
return false;
}
if (IsReparsePoint(*it) || time_expired()) {
return false;
}
}
return true;
}
bool IsSameDirectory(const base::FilePath& a, const base::FilePath& b) {
base::FilePath abs_a = base::MakeAbsoluteFilePath(a);
base::FilePath abs_b = base::MakeAbsoluteFilePath(b);
if (abs_a.empty() || abs_b.empty()) {
// MakeAbsoluteFilePath failed; fall back to case-insensitive compare.
return base::FilePath::CompareEqualIgnoreCase(a.value(), b.value());
}
return base::FilePath::CompareEqualIgnoreCase(abs_a.value(), abs_b.value());
}
const char* FileOpsErrorToString(FileOpsError error) {
switch (error) {
case FileOpsError::kSuccess:
return "Success";
case FileOpsError::kSourceNotFound:
return "Source directory not found";
case FileOpsError::kDestinationExists:
return "Destination already exists";
case FileOpsError::kDestinationNotFound:
return "Destination not found";
case FileOpsError::kAccessDenied:
return "Access denied";
case FileOpsError::kDiskFull:
return "Disk full";
case FileOpsError::kInUse:
return "Files are in use";
case FileOpsError::kRenameFailed:
return "Rename operation failed";
case FileOpsError::kDeleteFailed:
return "Delete operation failed";
case FileOpsError::kQuarantineFailed:
return "Destination quarantine failed";
case FileOpsError::kRepairFailed:
return "Destination repair failed";
}
return "Unknown error";
}
ScopedFileDeleter::ScopedFileDeleter(const base::FilePath& path)
: path_(path) {}
ScopedFileDeleter::~ScopedFileDeleter() {
if (!path_.empty()) {
base::DeleteFile(path_);
}
}
void ScopedFileDeleter::Release() {
path_ = base::FilePath();
}
ScopedDirectoryDeleter::ScopedDirectoryDeleter(const base::FilePath& path)
: path_(path) {}
ScopedDirectoryDeleter::~ScopedDirectoryDeleter() {
if (!path_.empty()) {
base::DeletePathRecursively(path_);
}
}
void ScopedDirectoryDeleter::Release() {
path_ = base::FilePath();
}
} // namespace cef_installer
@@ -0,0 +1,234 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_FILE_OPS_H_
#define CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_FILE_OPS_H_
#include <windows.h>
#include <memory>
#include <vector>
#include "base/files/file_path.h"
#include "base/time/time.h"
#include "cef/libcef_dll/bootstrap/installer/installer_version.h"
namespace cef_installer {
// Error codes for file operations
enum class FileOpsError {
kSuccess,
kSourceNotFound, // Source directory doesn't exist
kDestinationExists, // Destination already exists (for install)
kDestinationNotFound, // Destination doesn't exist (for uninstall)
kAccessDenied, // Permission denied
kDiskFull, // Not enough space
kInUse, // Files are in use by another process
kRenameFailed, // Atomic rename operation failed
kDeleteFailed, // Could not delete files
kQuarantineFailed, // Existing destination could not be quarantined
kRepairFailed, // Verified staging could not replace quarantine
};
enum class FileOpsFault {
kNone,
kQuarantineMove,
kRepairMove,
kTrashMove,
kTrashReclaim,
};
enum class VersionLeaseError {
kSuccess,
kLostRace,
kInvalidPath,
kOpenFailed,
};
// Move-only RAII lease that keeps a selected shared-store distribution usable.
// The fixed libcef.dll handle permits other readers but denies delete sharing,
// preventing a whole-version directory move until the final lease is released.
class VersionLease {
public:
~VersionLease();
VersionLease(VersionLease&& other) noexcept;
VersionLease& operator=(VersionLease&& other) noexcept;
VersionLease(const VersionLease&) = delete;
VersionLease& operator=(const VersionLease&) = delete;
bool IsValid() const;
private:
friend VersionLeaseError AcquireVersionLease(
const base::FilePath& trusted_root,
const base::FilePath& version_dir,
std::unique_ptr<VersionLease>* lease);
explicit VersionLease(HANDLE handle);
HANDLE handle_ = INVALID_HANDLE_VALUE;
};
// Acquire a lease for Release/libcef.dll beneath a validated installed version
// root. kLostRace means removal won and selection may be retried; other errors
// are hard validation/open failures.
VersionLeaseError AcquireVersionLease(const base::FilePath& trusted_root,
const base::FilePath& version_dir,
std::unique_ptr<VersionLease>* lease);
// Deterministic mutation-boundary faults. Typed E2E configuration maps the
// equivalent non-official subprocess values quarantine_move, repair_move,
// trash_move, and trash_reclaim at this component boundary.
void SetFileOpsFaultForTesting(FileOpsFault fault);
// Install a CEF version from extracted temp directory to final location.
// Uses atomic rename for safety (all-or-nothing).
//
// Prerequisites: Caller must have already verified the source directory
// contents (signature/catalog verification) before calling this function.
//
// Parameters:
// - source_dir: Operation-owned temp directory containing extracted and
// verified CEF files. It is consumed only by a successful rename.
// - install_dir: Base install directory (e.g., "C:\Program Files\CEF")
// - version: CEF version being installed
// - cleanup_deferred: Set when publication succeeds but reclamation of a
// quarantined target fails. The caller must surface this only after all
// later logical publication and result validation succeeds.
//
// Process:
// 1. Verify source_dir exists and is not a reparse point
// 2. Construct dest: <install_dir>\Versions\<version>\<platform>
// 3. Walk dest up to install_dir checking existing ancestors for reparse points
// 4. If dest exists as a safe regular file or directory, move it opaquely to
// reparse-safe .trash/. A quarantine failure preserves both dest and source.
// 5. Create parent directories if needed
// 6. Rename source_dir to dest (atomic, same volume). Failure after quarantine
// preserves source, leaves the old target in .trash/, and leaves dest
// absent.
// 7. Reclaim a quarantined target without traversing reparse points;
// reclamation failure is deferred physical cleanup and never rolls back
// publication.
//
// IMPORTANT: source_dir MUST be on the same volume as install_dir. The
// controller ensures this by staging under <install_dir>\.staging\.
// Cross-volume renames will fail with kRenameFailed.
FileOpsError InstallVersion(const base::FilePath& source_dir,
const base::FilePath& install_dir,
const Version& version,
bool* cleanup_deferred = nullptr);
// Remove a CEF version that is no longer needed.
//
// Process:
// 1. Verify source exists and is not a reparse point
// 2. Check intermediate directories for reparse points
// 3. Create <install_dir>\.trash\ if needed (with reparse point check)
// 4. Rename source to <install_dir>\.trash\<version>_<random> (atomic)
// 5. Delete trash entry recursively
// 6. If delete fails (files in use), leave for retry on next run
// 7. If parent version directory is empty, remove it too
//
// Returns kInUse if files are locked; caller should retry later.
// Uses current platform.
FileOpsError UninstallVersion(const base::FilePath& install_dir,
const Version& version);
// Uninstall a specific platform of a CEF version.
// Same as above but for a specified platform instead of current platform.
// Returns kInUse if files are locked; caller should retry later.
FileOpsError UninstallVersion(const base::FilePath& install_dir,
const Version& version,
const std::string& platform);
// Retry deletion of previously failed uninstalls.
// Called automatically at the start of each installer operation.
// Returns the number of successfully deleted trash entries (files or
// directories).
int RetryPendingDeletions(const base::FilePath& install_dir);
// Check if a path is a reparse point (symlink or junction).
// Used to prevent symlink/junction attacks where an attacker creates a
// junction pointing to a sensitive directory.
// Returns false if path doesn't exist or attributes can't be read.
bool IsReparsePoint(const base::FilePath& path);
// Verify a file path is safe (not a reparse point). If the path is a reparse
// point, removes the reparse point itself (not its target).
// If |delete_if_exists| is true, also deletes the file at the path.
// Returns true if the path is safe to use (was not a reparse point, or the
// reparse point was successfully removed). Returns false only if a reparse
// point exists but could not be removed.
bool VerifySafeFilePath(const base::FilePath& path,
bool delete_if_exists = false);
// Verify a directory path is safe (not a reparse point). If the path is a
// reparse point, removes the reparse point itself (not its target).
// If |delete_if_exists| is true, also deletes the directory recursively.
// Returns true if the path is safe to use (was not a reparse point, or the
// reparse point was successfully removed). Returns false only if a reparse
// point exists but could not be removed.
bool VerifySafeDirectoryPath(const base::FilePath& path,
bool delete_if_exists = false);
// Check if a directory exists and is not a reparse point (read-only check).
// Does NOT require write access. Does NOT create the directory.
bool IsReadableDirectory(const base::FilePath& path);
// Validate that every path component between |trusted_root| (exclusive) and
// |path| (inclusive) exists and is not a reparse point.
// Both paths must be absolute. |trusted_root| itself is not checked —
// assumed safe by the caller.
// Returns false if any component is a reparse point, does not exist, or
// either path is relative.
bool IsPathSafeForLoading(const base::FilePath& trusted_root,
const base::FilePath& path,
base::TimeTicks deadline = {},
bool* time_limit_reached = nullptr);
// Returns true if two paths resolve to the same directory.
// Uses MakeAbsoluteFilePath for canonicalization, with case-insensitive
// fallback comparison on Windows.
bool IsSameDirectory(const base::FilePath& a, const base::FilePath& b);
// Convert error code to human-readable string for logging
const char* FileOpsErrorToString(FileOpsError error);
// RAII helper that deletes a file when it goes out of scope.
// Call Release() to prevent deletion (e.g., after successful rename).
class ScopedFileDeleter {
public:
explicit ScopedFileDeleter(const base::FilePath& path);
~ScopedFileDeleter();
ScopedFileDeleter(const ScopedFileDeleter&) = delete;
ScopedFileDeleter& operator=(const ScopedFileDeleter&) = delete;
void Release();
private:
base::FilePath path_;
};
// RAII helper that recursively deletes a directory when it goes out of scope.
// Call Release() to prevent deletion.
class ScopedDirectoryDeleter {
public:
explicit ScopedDirectoryDeleter(const base::FilePath& path);
~ScopedDirectoryDeleter();
ScopedDirectoryDeleter(const ScopedDirectoryDeleter&) = delete;
ScopedDirectoryDeleter& operator=(const ScopedDirectoryDeleter&) = delete;
void Release();
private:
base::FilePath path_;
};
} // namespace cef_installer
#endif // CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_FILE_OPS_H_
@@ -0,0 +1,943 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cef/libcef_dll/bootstrap/installer/installer_file_ops.h"
#include <windows.h>
#include "base/command_line.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/test/file_path_reparse_point_win.h"
#include "cef/libcef_dll/bootstrap/installer/installer_constants.h"
#include "cef/libcef_dll/bootstrap/installer/installer_paths.h"
#include "cef/libcef_dll/bootstrap/installer/installer_version_metadata.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace cef_installer {
namespace {
TEST(InstallerVersionLeaseChildTest, HoldUntilKilled) {
const base::CommandLine* command = base::CommandLine::ForCurrentProcess();
base::FilePath trusted_root =
command->GetSwitchValuePath("cef-lease-child-root");
base::FilePath version_dir =
command->GetSwitchValuePath("cef-lease-child-version");
std::wstring event_name =
command->GetSwitchValueNative("cef-lease-child-event");
if (trusted_root.empty() || version_dir.empty() || event_name.empty()) {
GTEST_SKIP();
}
std::unique_ptr<VersionLease> lease;
ASSERT_EQ(VersionLeaseError::kSuccess,
AcquireVersionLease(trusted_root, version_dir, &lease));
HANDLE event = OpenEventW(EVENT_MODIFY_STATE, FALSE, event_name.c_str());
ASSERT_NE(nullptr, event);
ASSERT_TRUE(SetEvent(event));
CloseHandle(event);
WaitForSingleObject(GetCurrentProcess(), INFINITE);
}
class InstallerFileOpsTest : public testing::Test {
protected:
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
install_dir_ = temp_dir_.GetPath().Append(kCefSubdirectory);
ASSERT_TRUE(base::CreateDirectory(install_dir_));
}
void TearDown() override { SetFileOpsFaultForTesting(FileOpsFault::kNone); }
// Create a mock source directory with the version metadata file
base::FilePath CreateMockSourceDir(const std::string& version_str) {
base::FilePath source_dir = temp_dir_.GetPath().Append(
L"source_" + base::FilePath::FromUTF8Unsafe(version_str).value());
base::CreateDirectory(source_dir);
// Create the version metadata file
base::FilePath metadata_file = source_dir.Append(kVersionMetadataFilename);
base::WriteFile(metadata_file, R"({"version":")" + version_str + R"("})");
return source_dir;
}
// Create a mock installed version
void CreateInstalledVersion(const std::string& version_str) {
Version version = Version::Parse(version_str);
base::FilePath version_dir = GetVersionPath(install_dir_, version);
base::CreateDirectory(version_dir);
// Create the version metadata file
base::FilePath metadata_file = version_dir.Append(kVersionMetadataFilename);
base::WriteFile(metadata_file, R"({"version":")" + version_str + R"("})");
}
VersionMetadata CreateCompleteDistribution(
const base::FilePath& path,
const std::string& version_str,
const std::string& abi_hash = "abc123def4567890") {
EXPECT_TRUE(base::CreateDirectory(path));
VersionMetadata metadata;
metadata.version = Version::Parse(version_str);
metadata.abi_hash = abi_hash;
metadata.platform = GetCurrentPlatform();
EXPECT_EQ(MetadataError::kSuccess, WriteVersionMetadata(path, metadata));
base::FilePath release = path.Append(kReleaseSubdirectory);
EXPECT_TRUE(base::CreateDirectory(release));
EXPECT_TRUE(base::WriteFile(release.Append(kLibcefFilename), "libcef"));
EXPECT_TRUE(base::WriteFile(path.Append(kCatalogFilename), "catalog"));
return metadata;
}
base::ScopedTempDir temp_dir_;
base::FilePath install_dir_;
};
// =============================================================================
// InstallVersion Tests
// =============================================================================
TEST_F(InstallerFileOpsTest, InstallVersionSuccess) {
base::FilePath source_dir = CreateMockSourceDir("137.3.5");
Version version = Version::Parse("137.3.5");
FileOpsError result = InstallVersion(source_dir, install_dir_, version);
EXPECT_EQ(FileOpsError::kSuccess, result);
// Verify the version was installed
base::FilePath installed_path = GetVersionPath(install_dir_, version);
EXPECT_TRUE(base::DirectoryExists(installed_path));
EXPECT_TRUE(
base::PathExists(installed_path.Append(kVersionMetadataFilename)));
// Source should no longer exist (was renamed)
EXPECT_FALSE(base::DirectoryExists(source_dir));
}
TEST_F(InstallerFileOpsTest, InstallVersionDestExists) {
const std::string version_string = "137.3.5";
Version version = Version::Parse(version_string);
CreateInstalledVersion("137.3.5");
base::FilePath destination = GetVersionPath(install_dir_, version);
ASSERT_TRUE(
base::WriteFile(destination.Append(L"old_marker.txt"), "old target"));
base::FilePath source_dir = CreateMockSourceDir(version_string);
ASSERT_TRUE(
base::WriteFile(source_dir.Append(L"staging_marker.txt"), "staging"));
FileOpsError result = InstallVersion(source_dir, install_dir_, version);
EXPECT_EQ(FileOpsError::kSuccess, result);
EXPECT_FALSE(base::PathExists(source_dir));
EXPECT_FALSE(base::PathExists(destination.Append(L"old_marker.txt")));
EXPECT_TRUE(base::PathExists(destination.Append(L"staging_marker.txt")));
}
TEST_F(InstallerFileOpsTest, InstallVersionSourceMissing) {
base::FilePath nonexistent_dir = temp_dir_.GetPath().Append(L"nonexistent");
Version version = Version::Parse("137.3.5");
FileOpsError result = InstallVersion(nonexistent_dir, install_dir_, version);
EXPECT_EQ(FileOpsError::kSourceNotFound, result);
}
TEST_F(InstallerFileOpsTest, InstallVersionInvalidVersion) {
base::FilePath source_dir = CreateMockSourceDir("137.3.5");
Version invalid_version; // Default-constructed = invalid
FileOpsError result =
InstallVersion(source_dir, install_dir_, invalid_version);
EXPECT_EQ(FileOpsError::kSourceNotFound, result);
}
TEST_F(InstallerFileOpsTest, InstallVersionCreatesVersionsDir) {
// Use a fresh install dir without Versions subdirectory
base::FilePath fresh_install_dir = temp_dir_.GetPath().Append(L"FreshCEF");
ASSERT_TRUE(base::CreateDirectory(fresh_install_dir));
base::FilePath source_dir = CreateMockSourceDir("137.3.5");
Version version = Version::Parse("137.3.5");
FileOpsError result = InstallVersion(source_dir, fresh_install_dir, version);
EXPECT_EQ(FileOpsError::kSuccess, result);
// Versions subdirectory should have been created
base::FilePath versions_dir = fresh_install_dir.Append(kVersionsSubdirectory);
EXPECT_TRUE(base::DirectoryExists(versions_dir));
}
TEST_F(InstallerFileOpsTest, ReplacesPartialDestination) {
const std::string version_string = "137.3.5";
Version version = Version::Parse(version_string);
base::FilePath source = temp_dir_.GetPath().Append(L"replacement_source");
VersionMetadata expected = CreateCompleteDistribution(source, version_string);
ASSERT_TRUE(base::WriteFile(source.Append(L"staging_marker.txt"), "staging"));
base::FilePath destination = GetVersionPath(install_dir_, version);
ASSERT_TRUE(base::CreateDirectory(destination));
ASSERT_TRUE(base::WriteFile(destination.Append(L"partial.txt"), "partial"));
bool cleanup_deferred = false;
EXPECT_EQ(FileOpsError::kSuccess,
InstallVersion(source, install_dir_, version, &cleanup_deferred));
EXPECT_EQ(DistributionValidation::kComplete,
ValidateDistribution(install_dir_, destination, expected));
EXPECT_FALSE(base::PathExists(destination.Append(L"partial.txt")));
EXPECT_TRUE(base::PathExists(destination.Append(L"staging_marker.txt")));
EXPECT_FALSE(cleanup_deferred);
}
TEST_F(InstallerFileOpsTest, ReplacesCompleteDestinationWithStaging) {
const std::string version_string = "137.3.5";
Version version = Version::Parse(version_string);
base::FilePath source = temp_dir_.GetPath().Append(L"verified_source");
VersionMetadata expected = CreateCompleteDistribution(source, version_string);
ASSERT_TRUE(base::WriteFile(
source.Append(kReleaseSubdirectory).Append(kLibcefFilename),
"verified staging bytes"));
ASSERT_TRUE(base::WriteFile(source.Append(L"staging_marker.txt"), "staging"));
base::FilePath destination = GetVersionPath(install_dir_, version);
CreateCompleteDistribution(destination, version_string);
ASSERT_TRUE(base::WriteFile(
destination.Append(kReleaseSubdirectory).Append(kLibcefFilename),
"orphan bytes"));
ASSERT_TRUE(
base::WriteFile(destination.Append(L"old_marker.txt"), "old target"));
bool cleanup_deferred = false;
EXPECT_EQ(FileOpsError::kSuccess,
InstallVersion(source, install_dir_, version, &cleanup_deferred));
EXPECT_FALSE(base::PathExists(source));
EXPECT_EQ(DistributionValidation::kComplete,
ValidateDistribution(install_dir_, destination, expected));
std::string installed_bytes;
ASSERT_TRUE(base::ReadFileToString(
destination.Append(kReleaseSubdirectory).Append(kLibcefFilename),
&installed_bytes));
EXPECT_EQ("verified staging bytes", installed_bytes);
EXPECT_FALSE(base::PathExists(destination.Append(L"old_marker.txt")));
EXPECT_TRUE(base::PathExists(destination.Append(L"staging_marker.txt")));
EXPECT_FALSE(cleanup_deferred);
}
TEST_F(InstallerFileOpsTest, ReplacesEmptyDirectoryDestination) {
const std::string version_string = "137.3.5";
Version version = Version::Parse(version_string);
base::FilePath source = temp_dir_.GetPath().Append(L"empty_target_source");
CreateCompleteDistribution(source, version_string);
ASSERT_TRUE(base::WriteFile(source.Append(L"staging_marker.txt"), "staging"));
base::FilePath destination = GetVersionPath(install_dir_, version);
ASSERT_TRUE(base::CreateDirectory(destination));
EXPECT_EQ(FileOpsError::kSuccess,
InstallVersion(source, install_dir_, version));
EXPECT_TRUE(base::PathExists(destination.Append(L"staging_marker.txt")));
}
TEST_F(InstallerFileOpsTest, ReplacesRegularFileDestination) {
const std::string version_string = "137.3.5";
Version version = Version::Parse(version_string);
base::FilePath source = temp_dir_.GetPath().Append(L"file_target_source");
CreateCompleteDistribution(source, version_string);
ASSERT_TRUE(base::WriteFile(source.Append(L"staging_marker.txt"), "staging"));
base::FilePath destination = GetVersionPath(install_dir_, version);
ASSERT_TRUE(base::CreateDirectory(destination.DirName()));
ASSERT_TRUE(base::WriteFile(destination, "old regular file"));
EXPECT_EQ(FileOpsError::kSuccess,
InstallVersion(source, install_dir_, version));
EXPECT_TRUE(base::DirectoryExists(destination));
EXPECT_TRUE(base::PathExists(destination.Append(L"staging_marker.txt")));
}
TEST_F(InstallerFileOpsTest, RejectsReparsePointDestination) {
const std::string version_string = "137.3.5";
Version version = Version::Parse(version_string);
base::FilePath source = temp_dir_.GetPath().Append(L"reparse_source");
CreateCompleteDistribution(source, version_string);
base::FilePath external = temp_dir_.GetPath().Append(L"external_target");
ASSERT_TRUE(base::CreateDirectory(external));
ASSERT_TRUE(base::WriteFile(external.Append(L"sentinel.txt"), "sentinel"));
base::FilePath destination = GetVersionPath(install_dir_, version);
ASSERT_TRUE(base::CreateDirectory(destination));
auto reparse_point =
base::test::FilePathReparsePoint::Create(destination, external);
ASSERT_TRUE(reparse_point.has_value());
EXPECT_EQ(FileOpsError::kQuarantineFailed,
InstallVersion(source, install_dir_, version));
EXPECT_TRUE(base::PathExists(source));
EXPECT_TRUE(base::PathExists(destination));
EXPECT_TRUE(base::PathExists(external.Append(L"sentinel.txt")));
EXPECT_FALSE(base::PathExists(install_dir_.Append(kTrashSubdirectory)));
}
TEST_F(InstallerFileOpsTest, ReplacementFaultBoundaries) {
const std::string version_string = "137.3.5";
Version version = Version::Parse(version_string);
for (FileOpsFault fault :
{FileOpsFault::kQuarantineMove, FileOpsFault::kRepairMove}) {
base::FilePath source = temp_dir_.GetPath().Append(
fault == FileOpsFault::kQuarantineMove ? L"quarantine_source"
: L"repair_move_source");
CreateCompleteDistribution(source, version_string);
ASSERT_TRUE(
base::WriteFile(source.Append(L"staging_marker.txt"), "staging"));
base::FilePath destination = GetVersionPath(install_dir_, version);
ASSERT_TRUE(base::CreateDirectory(destination));
ASSERT_TRUE(
base::WriteFile(destination.Append(L"old_marker.txt"), "old target"));
SetFileOpsFaultForTesting(fault);
FileOpsError error = InstallVersion(source, install_dir_, version);
SetFileOpsFaultForTesting(FileOpsFault::kNone);
EXPECT_EQ(fault == FileOpsFault::kQuarantineMove
? FileOpsError::kQuarantineFailed
: FileOpsError::kRepairFailed,
error);
EXPECT_TRUE(base::PathExists(source));
base::FilePath trash = install_dir_.Append(kTrashSubdirectory);
base::FileEnumerator entries(
trash, false,
base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES);
base::FilePath quarantined = entries.Next();
if (fault == FileOpsFault::kQuarantineMove) {
EXPECT_TRUE(base::PathExists(destination.Append(L"old_marker.txt")));
EXPECT_TRUE(quarantined.empty());
} else {
EXPECT_FALSE(base::PathExists(destination));
ASSERT_FALSE(quarantined.empty());
EXPECT_TRUE(base::PathExists(quarantined.Append(L"old_marker.txt")));
}
base::DeletePathRecursively(destination);
base::DeletePathRecursively(source);
base::DeletePathRecursively(trash);
}
}
TEST_F(InstallerFileOpsTest, ReplacementCleanupDeferredAndRetryable) {
const std::string version_string = "137.3.5";
Version version = Version::Parse(version_string);
base::FilePath source = temp_dir_.GetPath().Append(L"deferred_source");
CreateCompleteDistribution(source, version_string);
ASSERT_TRUE(base::WriteFile(source.Append(L"staging_marker.txt"), "staging"));
base::FilePath destination = GetVersionPath(install_dir_, version);
ASSERT_TRUE(base::CreateDirectory(destination));
ASSERT_TRUE(
base::WriteFile(destination.Append(L"old_marker.txt"), "old target"));
bool cleanup_deferred = false;
SetFileOpsFaultForTesting(FileOpsFault::kTrashReclaim);
EXPECT_EQ(FileOpsError::kSuccess,
InstallVersion(source, install_dir_, version, &cleanup_deferred));
SetFileOpsFaultForTesting(FileOpsFault::kNone);
EXPECT_TRUE(cleanup_deferred);
EXPECT_TRUE(base::PathExists(destination.Append(L"staging_marker.txt")));
EXPECT_FALSE(base::PathExists(destination.Append(L"old_marker.txt")));
EXPECT_EQ(1, RetryPendingDeletions(install_dir_));
}
TEST_F(InstallerFileOpsTest, TrashFaultBoundaries) {
const std::string version_string = "137.3.5";
Version version = Version::Parse(version_string);
CreateInstalledVersion(version_string);
SetFileOpsFaultForTesting(FileOpsFault::kTrashMove);
EXPECT_EQ(FileOpsError::kRenameFailed,
UninstallVersion(install_dir_, version));
SetFileOpsFaultForTesting(FileOpsFault::kNone);
EXPECT_TRUE(base::PathExists(GetVersionPath(install_dir_, version)));
SetFileOpsFaultForTesting(FileOpsFault::kTrashReclaim);
EXPECT_EQ(FileOpsError::kInUse, UninstallVersion(install_dir_, version));
SetFileOpsFaultForTesting(FileOpsFault::kNone);
EXPECT_FALSE(base::PathExists(GetVersionPath(install_dir_, version)));
base::FilePath trash = install_dir_.Append(kTrashSubdirectory);
base::FileEnumerator entries(trash, false, base::FileEnumerator::DIRECTORIES);
EXPECT_FALSE(entries.Next().empty());
}
// =============================================================================
// UninstallVersion Tests
// =============================================================================
TEST_F(InstallerFileOpsTest, UninstallVersionSuccess) {
CreateInstalledVersion("137.3.5");
Version version = Version::Parse("137.3.5");
// Verify it exists before uninstall
base::FilePath version_path = GetVersionPath(install_dir_, version);
ASSERT_TRUE(base::DirectoryExists(version_path));
FileOpsError result = UninstallVersion(install_dir_, version);
EXPECT_EQ(FileOpsError::kSuccess, result);
// Version directory should be gone
EXPECT_FALSE(base::DirectoryExists(version_path));
}
TEST_F(InstallerFileOpsTest, UninstallVersionNotFound) {
Version version = Version::Parse("999.0.0");
FileOpsError result = UninstallVersion(install_dir_, version);
EXPECT_EQ(FileOpsError::kDestinationNotFound, result);
}
TEST_F(InstallerFileOpsTest, UninstallVersionInUse) {
CreateInstalledVersion("137.3.5");
Version version = Version::Parse("137.3.5");
base::FilePath version_path = GetVersionPath(install_dir_, version);
// Lock a file in the directory to simulate "in use"
base::FilePath locked_file = version_path.Append(L"locked.dll");
base::WriteFile(locked_file, "test");
// Open with exclusive access to lock it
HANDLE handle =
::CreateFileW(locked_file.value().c_str(), GENERIC_READ | GENERIC_WRITE,
0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
ASSERT_NE(INVALID_HANDLE_VALUE, handle);
FileOpsError result = UninstallVersion(install_dir_, version);
// When a file is locked, different things can happen:
// - kInUse: MoveFileEx detects sharing violation
// - kAccessDenied: MoveFileEx returns access denied for locked directory
// - kSuccess: Rename succeeded but delete failed (kInUse semantics)
// All are acceptable outcomes for a locked file scenario
EXPECT_TRUE(result == FileOpsError::kInUse ||
result == FileOpsError::kAccessDenied ||
result == FileOpsError::kSuccess)
<< "Unexpected error: " << FileOpsErrorToString(result);
::CloseHandle(handle);
// Clean up - retry deletions
RetryPendingDeletions(install_dir_);
}
TEST_F(InstallerFileOpsTest, UninstallVersionCleansEmptyParent) {
CreateInstalledVersion("137.3.5");
Version version = Version::Parse("137.3.5");
// Verify parent version dir exists: Versions/137.3.5/
base::FilePath version_parent =
install_dir_.Append(kVersionsSubdirectory).Append(L"137.3.5");
ASSERT_TRUE(base::DirectoryExists(version_parent));
// Verify platform dir exists: Versions/137.3.5/<platform>/
base::FilePath version_path = GetVersionPath(install_dir_, version);
ASSERT_TRUE(base::DirectoryExists(version_path));
FileOpsError result = UninstallVersion(install_dir_, version);
EXPECT_EQ(FileOpsError::kSuccess, result);
// Platform directory should be gone
EXPECT_FALSE(base::DirectoryExists(version_path));
// Parent version directory should also be cleaned up since it's now empty
EXPECT_FALSE(base::DirectoryExists(version_parent));
}
TEST_F(InstallerFileOpsTest, VersionLeaseBlocksWholeDirectoryMove) {
const Version version = Version::Parse("137.3.5");
const base::FilePath version_path = GetVersionPath(install_dir_, version);
CreateCompleteDistribution(version_path, version.ToString());
std::unique_ptr<VersionLease> lease;
ASSERT_EQ(VersionLeaseError::kSuccess,
AcquireVersionLease(install_dir_, version_path, &lease));
ASSERT_TRUE(lease);
EXPECT_TRUE(lease->IsValid());
const base::FilePath moved = temp_dir_.GetPath().Append(L"leased_moved");
EXPECT_FALSE(MoveFileExW(version_path.value().c_str(), moved.value().c_str(),
MOVEFILE_WRITE_THROUGH));
EXPECT_TRUE(base::DirectoryExists(version_path));
lease.reset();
EXPECT_TRUE(MoveFileExW(version_path.value().c_str(), moved.value().c_str(),
MOVEFILE_WRITE_THROUGH));
}
TEST_F(InstallerFileOpsTest, MultipleVersionLeaseReaders) {
const Version version = Version::Parse("137.3.5");
const base::FilePath version_path = GetVersionPath(install_dir_, version);
CreateCompleteDistribution(version_path, version.ToString());
std::unique_ptr<VersionLease> first;
std::unique_ptr<VersionLease> second;
EXPECT_EQ(VersionLeaseError::kSuccess,
AcquireVersionLease(install_dir_, version_path, &first));
EXPECT_EQ(VersionLeaseError::kSuccess,
AcquireVersionLease(install_dir_, version_path, &second));
EXPECT_TRUE(first);
EXPECT_TRUE(second);
}
TEST_F(InstallerFileOpsTest, VersionLeaseDetectsLostRemovalRace) {
const Version version = Version::Parse("137.3.5");
const base::FilePath version_path = GetVersionPath(install_dir_, version);
std::unique_ptr<VersionLease> lease;
EXPECT_EQ(VersionLeaseError::kLostRace,
AcquireVersionLease(install_dir_, version_path, &lease));
EXPECT_FALSE(lease);
}
TEST_F(InstallerFileOpsTest, ProcessTerminationReleasesVersionLease) {
const Version version = Version::Parse("137.3.5");
const base::FilePath version_path = GetVersionPath(install_dir_, version);
CreateCompleteDistribution(version_path, version.ToString());
std::wstring event_name = std::wstring(L"Local") + static_cast<wchar_t>(92) +
L"CEF_Installer_Lease_Test_" +
std::to_wstring(GetCurrentProcessId()) + L"_" +
std::to_wstring(GetTickCount64());
HANDLE event = CreateEventW(nullptr, TRUE, FALSE, event_name.c_str());
ASSERT_NE(nullptr, event);
wchar_t executable[MAX_PATH] = {};
ASSERT_GT(GetModuleFileNameW(nullptr, executable, MAX_PATH), 0u);
const wchar_t quote = static_cast<wchar_t>(34);
std::wstring command_line(1, quote);
command_line += executable;
command_line += quote;
command_line +=
L" --single-process-tests "
L"--gtest_filter=InstallerVersionLeaseChildTest.HoldUntilKilled "
L"--cef-lease-child-root=";
command_line += quote;
command_line += install_dir_.value();
command_line += quote;
command_line += L" --cef-lease-child-version=";
command_line += quote;
command_line += version_path.value();
command_line += quote;
command_line += L" --cef-lease-child-event=" + event_name;
STARTUPINFOW startup = {};
startup.cb = sizeof(startup);
PROCESS_INFORMATION process = {};
ASSERT_TRUE(CreateProcessW(nullptr, command_line.data(), nullptr, nullptr,
FALSE, CREATE_NO_WINDOW, nullptr, nullptr,
&startup, &process));
CloseHandle(process.hThread);
ASSERT_EQ(WAIT_OBJECT_0, WaitForSingleObject(event, 10000));
CloseHandle(event);
const base::FilePath moved =
temp_dir_.GetPath().Append(L"process_lease_moved");
EXPECT_FALSE(MoveFileExW(version_path.value().c_str(), moved.value().c_str(),
MOVEFILE_WRITE_THROUGH));
ASSERT_TRUE(TerminateProcess(process.hProcess, 1));
ASSERT_EQ(WAIT_OBJECT_0, WaitForSingleObject(process.hProcess, 10000));
CloseHandle(process.hProcess);
EXPECT_TRUE(MoveFileExW(version_path.value().c_str(), moved.value().c_str(),
MOVEFILE_WRITE_THROUGH));
}
// =============================================================================
// RetryPendingDeletions Tests
// =============================================================================
TEST_F(InstallerFileOpsTest, RetryPendingDeletions) {
// Create .trash/ with entries
base::FilePath trash_root = install_dir_.Append(L".trash");
ASSERT_TRUE(base::CreateDirectory(trash_root));
base::FilePath trash1 = trash_root.Append(L"137.3.5_ABCDEF1234567890");
base::FilePath trash2 = trash_root.Append(L"138.0.0_1234567890ABCDEF");
ASSERT_TRUE(base::CreateDirectory(trash1));
ASSERT_TRUE(base::CreateDirectory(trash2));
// Add some files to make them non-empty
base::WriteFile(trash1.Append(L"file.txt"), "test");
base::WriteFile(trash2.Append(L"file.txt"), "test");
int deleted = RetryPendingDeletions(install_dir_);
EXPECT_EQ(2, deleted);
EXPECT_FALSE(base::DirectoryExists(trash1));
EXPECT_FALSE(base::DirectoryExists(trash2));
}
TEST_F(InstallerFileOpsTest, RetryPendingDeletionsPartialSuccess) {
// Create .trash/ with two entries
base::FilePath trash_root = install_dir_.Append(L".trash");
ASSERT_TRUE(base::CreateDirectory(trash_root));
base::FilePath trash1 = trash_root.Append(L"137.3.5_ABCDEF1234567890");
base::FilePath trash2 = trash_root.Append(L"138.0.0_1234567890ABCDEF");
ASSERT_TRUE(base::CreateDirectory(trash1));
ASSERT_TRUE(base::CreateDirectory(trash2));
// Lock a file in trash1
base::FilePath locked_file = trash1.Append(L"locked.dll");
base::WriteFile(locked_file, "test");
HANDLE handle =
::CreateFileW(locked_file.value().c_str(), GENERIC_READ | GENERIC_WRITE,
0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
ASSERT_NE(INVALID_HANDLE_VALUE, handle);
int deleted = RetryPendingDeletions(install_dir_);
// trash2 should be deleted, trash1 should remain (locked)
EXPECT_EQ(1, deleted);
EXPECT_TRUE(base::DirectoryExists(trash1));
EXPECT_FALSE(base::DirectoryExists(trash2));
::CloseHandle(handle);
// Now retry - should delete the remaining one
deleted = RetryPendingDeletions(install_dir_);
EXPECT_EQ(1, deleted);
EXPECT_FALSE(base::DirectoryExists(trash1));
}
TEST_F(InstallerFileOpsTest, RetryPendingDeletionsRegularFileTrash) {
base::FilePath trash_root = install_dir_.Append(kTrashSubdirectory);
ASSERT_TRUE(base::CreateDirectory(trash_root));
base::FilePath trash_file = trash_root.Append(L"137.3.5_file");
ASSERT_TRUE(base::WriteFile(trash_file, "old target"));
EXPECT_EQ(1, RetryPendingDeletions(install_dir_));
EXPECT_FALSE(base::PathExists(trash_file));
}
TEST_F(InstallerFileOpsTest,
RetryPendingDeletionsTrashDoesNotTraverseReparsePoint) {
base::FilePath external = temp_dir_.GetPath().Append(L"external_reclaim");
ASSERT_TRUE(base::CreateDirectory(external));
base::FilePath sentinel = external.Append(L"sentinel.txt");
ASSERT_TRUE(base::WriteFile(sentinel, "preserve"));
base::FilePath trash_root = install_dir_.Append(kTrashSubdirectory);
base::FilePath trash_entry = trash_root.Append(L"137.3.5_reparse");
base::FilePath nested = trash_entry.Append(L"nested");
ASSERT_TRUE(base::CreateDirectory(nested));
auto reparse_point =
base::test::FilePathReparsePoint::Create(nested, external);
ASSERT_TRUE(reparse_point.has_value());
ASSERT_TRUE(base::PathExists(nested.Append(L"sentinel.txt")));
EXPECT_EQ(1, RetryPendingDeletions(install_dir_));
EXPECT_FALSE(base::PathExists(trash_entry));
EXPECT_TRUE(base::PathExists(sentinel));
}
// =============================================================================
// FileOpsErrorToString Tests
// =============================================================================
TEST_F(InstallerFileOpsTest, FileOpsErrorToString) {
EXPECT_STREQ("Success", FileOpsErrorToString(FileOpsError::kSuccess));
EXPECT_STREQ("Source directory not found",
FileOpsErrorToString(FileOpsError::kSourceNotFound));
EXPECT_STREQ("Destination already exists",
FileOpsErrorToString(FileOpsError::kDestinationExists));
EXPECT_STREQ("Destination not found",
FileOpsErrorToString(FileOpsError::kDestinationNotFound));
EXPECT_STREQ("Access denied",
FileOpsErrorToString(FileOpsError::kAccessDenied));
EXPECT_STREQ("Disk full", FileOpsErrorToString(FileOpsError::kDiskFull));
EXPECT_STREQ("Files are in use", FileOpsErrorToString(FileOpsError::kInUse));
EXPECT_STREQ("Rename operation failed",
FileOpsErrorToString(FileOpsError::kRenameFailed));
EXPECT_STREQ("Delete operation failed",
FileOpsErrorToString(FileOpsError::kDeleteFailed));
}
TEST_F(InstallerFileOpsTest, IsReparsePointRegularDir) {
// A regular directory should not be a reparse point
EXPECT_FALSE(IsReparsePoint(temp_dir_.GetPath()));
EXPECT_FALSE(IsReparsePoint(install_dir_));
}
TEST_F(InstallerFileOpsTest, IsReparsePointNonExistent) {
// Non-existent path should return false
base::FilePath nonexistent = temp_dir_.GetPath().Append(L"does_not_exist");
EXPECT_FALSE(IsReparsePoint(nonexistent));
}
// =============================================================================
// VerifySafeFilePath Tests
// =============================================================================
TEST_F(InstallerFileOpsTest, VerifySafeFilePathRegularFile) {
base::FilePath file = temp_dir_.GetPath().Append(L"safe.txt");
base::WriteFile(file, "content");
EXPECT_TRUE(VerifySafeFilePath(file));
// File should still exist when delete_if_exists is false.
EXPECT_TRUE(base::PathExists(file));
}
TEST_F(InstallerFileOpsTest, VerifySafeFilePathNonExistent) {
base::FilePath missing = temp_dir_.GetPath().Append(L"missing.txt");
EXPECT_TRUE(VerifySafeFilePath(missing));
}
TEST_F(InstallerFileOpsTest, VerifySafeFilePathDeleteIfExists) {
base::FilePath file = temp_dir_.GetPath().Append(L"to_delete.txt");
base::WriteFile(file, "content");
EXPECT_TRUE(VerifySafeFilePath(file, /*delete_if_exists=*/true));
EXPECT_FALSE(base::PathExists(file));
}
TEST_F(InstallerFileOpsTest, VerifySafeFilePathDeleteNonExistent) {
base::FilePath missing = temp_dir_.GetPath().Append(L"missing.txt");
// Should succeed even if file doesn't exist.
EXPECT_TRUE(VerifySafeFilePath(missing, /*delete_if_exists=*/true));
}
// =============================================================================
// VerifySafeDirectoryPath Tests
// =============================================================================
TEST_F(InstallerFileOpsTest, VerifySafeDirectoryPathRegularDir) {
EXPECT_TRUE(VerifySafeDirectoryPath(install_dir_));
// Directory should still exist when delete_if_exists is false.
EXPECT_TRUE(base::DirectoryExists(install_dir_));
}
TEST_F(InstallerFileOpsTest, VerifySafeDirectoryPathNonExistent) {
base::FilePath missing = temp_dir_.GetPath().Append(L"missing_dir");
EXPECT_TRUE(VerifySafeDirectoryPath(missing));
}
TEST_F(InstallerFileOpsTest, VerifySafeDirectoryPathDeleteIfExists) {
base::FilePath dir = temp_dir_.GetPath().Append(L"dir_to_delete");
ASSERT_TRUE(base::CreateDirectory(dir));
base::WriteFile(dir.Append(L"child.txt"), "data");
EXPECT_TRUE(VerifySafeDirectoryPath(dir, /*delete_if_exists=*/true));
EXPECT_FALSE(base::DirectoryExists(dir));
}
TEST_F(InstallerFileOpsTest, VerifySafeDirectoryPathDeleteNonExistent) {
base::FilePath missing = temp_dir_.GetPath().Append(L"missing_dir");
EXPECT_TRUE(VerifySafeDirectoryPath(missing, /*delete_if_exists=*/true));
}
// =============================================================================
// IsReadableDirectory Tests
// =============================================================================
TEST_F(InstallerFileOpsTest, IsReadableDirectorySuccess) {
EXPECT_TRUE(IsReadableDirectory(install_dir_));
}
TEST_F(InstallerFileOpsTest, IsReadableDirectoryNonExistent) {
base::FilePath nonexistent = temp_dir_.GetPath().Append(L"does_not_exist");
EXPECT_FALSE(IsReadableDirectory(nonexistent));
}
TEST_F(InstallerFileOpsTest, IsReadableDirectoryEmptyPath) {
EXPECT_FALSE(IsReadableDirectory(base::FilePath()));
}
TEST_F(InstallerFileOpsTest, IsReadableDirectoryFile) {
base::FilePath file_path = temp_dir_.GetPath().Append(L"afile.txt");
base::WriteFile(file_path, "content");
EXPECT_FALSE(IsReadableDirectory(file_path));
}
// =============================================================================
// IsPathSafeForLoading Tests
// =============================================================================
TEST_F(InstallerFileOpsTest, IsPathSafeForLoadingNormalHierarchy) {
// Create root/a/b/file.dll
base::FilePath root = temp_dir_.GetPath().Append(L"root");
base::FilePath a = root.Append(L"a");
base::FilePath b = a.Append(L"b");
ASSERT_TRUE(base::CreateDirectory(b));
base::FilePath file = b.Append(L"file.dll");
ASSERT_TRUE(base::WriteFile(file, "content"));
EXPECT_TRUE(IsPathSafeForLoading(root, file));
}
TEST_F(InstallerFileOpsTest, IsPathSafeForLoadingNonexistentComponent) {
base::FilePath root = temp_dir_.GetPath().Append(L"root");
ASSERT_TRUE(base::CreateDirectory(root));
base::FilePath missing = root.Append(L"missing").Append(L"file.dll");
EXPECT_FALSE(IsPathSafeForLoading(root, missing));
}
TEST_F(InstallerFileOpsTest, IsPathSafeForLoadingPathNotUnderRoot) {
base::FilePath root = temp_dir_.GetPath().Append(L"root");
base::FilePath other = temp_dir_.GetPath().Append(L"other");
ASSERT_TRUE(base::CreateDirectory(root));
ASSERT_TRUE(base::CreateDirectory(other));
base::FilePath file = other.Append(L"file.dll");
ASSERT_TRUE(base::WriteFile(file, "content"));
EXPECT_FALSE(IsPathSafeForLoading(root, file));
}
TEST_F(InstallerFileOpsTest, IsPathSafeForLoadingEmptyRoot) {
EXPECT_FALSE(IsPathSafeForLoading(base::FilePath(), install_dir_));
}
TEST_F(InstallerFileOpsTest, IsPathSafeForLoadingEmptyPath) {
EXPECT_FALSE(IsPathSafeForLoading(install_dir_, base::FilePath()));
}
TEST_F(InstallerFileOpsTest, IsPathSafeForLoadingRootEqualsPath) {
// Path is not under root (IsParent returns false when they're equal)
EXPECT_FALSE(IsPathSafeForLoading(install_dir_, install_dir_));
}
TEST_F(InstallerFileOpsTest, IsPathSafeForLoadingRelativePaths) {
EXPECT_FALSE(
IsPathSafeForLoading(base::FilePath(L"relative\\root"),
base::FilePath(L"relative\\root\\file.dll")));
}
TEST_F(InstallerFileOpsTest, IsPathSafeForLoadingHonorsSoftDeadline) {
bool time_limit_reached = false;
EXPECT_FALSE(
IsPathSafeForLoading(install_dir_, install_dir_.Append(L"file.dll"),
base::TimeTicks::Now(), &time_limit_reached));
EXPECT_TRUE(time_limit_reached);
}
// =============================================================================
// IsSameDirectory Tests
// =============================================================================
TEST_F(InstallerFileOpsTest, IsSameDirectorySamePath) {
EXPECT_TRUE(IsSameDirectory(install_dir_, install_dir_));
}
TEST_F(InstallerFileOpsTest, IsSameDirectoryDifferentCase) {
// On Windows, paths differing only in case should be the same.
base::FilePath upper(L"C:\\WINDOWS\\TEMP");
base::FilePath lower(L"C:\\windows\\temp");
EXPECT_TRUE(IsSameDirectory(upper, lower));
}
TEST_F(InstallerFileOpsTest, IsSameDirectoryDifferentPaths) {
base::FilePath dir_a = temp_dir_.GetPath().Append(L"DirA");
base::FilePath dir_b = temp_dir_.GetPath().Append(L"DirB");
ASSERT_TRUE(base::CreateDirectory(dir_a));
ASSERT_TRUE(base::CreateDirectory(dir_b));
EXPECT_FALSE(IsSameDirectory(dir_a, dir_b));
}
// =============================================================================
// ScopedFileDeleter Tests
// =============================================================================
TEST_F(InstallerFileOpsTest, ScopedFileDeleterDeletesOnDestruction) {
base::FilePath file = temp_dir_.GetPath().Append(L"to_delete.txt");
base::WriteFile(file, "content");
ASSERT_TRUE(base::PathExists(file));
{
ScopedFileDeleter deleter(file);
}
EXPECT_FALSE(base::PathExists(file));
}
TEST_F(InstallerFileOpsTest, ScopedFileDeleterReleasePreventsDeletion) {
base::FilePath file = temp_dir_.GetPath().Append(L"keep.txt");
base::WriteFile(file, "content");
ASSERT_TRUE(base::PathExists(file));
{
ScopedFileDeleter deleter(file);
deleter.Release();
}
EXPECT_TRUE(base::PathExists(file));
}
TEST_F(InstallerFileOpsTest, ScopedFileDeleterEmptyPathIsNoOp) {
// Should not crash when constructed with an empty path.
{
ScopedFileDeleter deleter{base::FilePath()};
}
}
TEST_F(InstallerFileOpsTest, ScopedFileDeleterNonexistentPathIsNoOp) {
base::FilePath missing = temp_dir_.GetPath().Append(L"no_such_file.txt");
ASSERT_FALSE(base::PathExists(missing));
// Should not crash when the file doesn't exist.
{
ScopedFileDeleter deleter(missing);
}
}
// =============================================================================
// ScopedDirectoryDeleter Tests
// =============================================================================
TEST_F(InstallerFileOpsTest, ScopedDirectoryDeleterDeletesOnDestruction) {
base::FilePath dir = temp_dir_.GetPath().Append(L"dir_to_delete");
ASSERT_TRUE(base::CreateDirectory(dir));
base::WriteFile(dir.Append(L"child.txt"), "data");
ASSERT_TRUE(base::DirectoryExists(dir));
{
ScopedDirectoryDeleter deleter(dir);
}
EXPECT_FALSE(base::DirectoryExists(dir));
}
TEST_F(InstallerFileOpsTest, ScopedDirectoryDeleterReleasePreventsDeletion) {
base::FilePath dir = temp_dir_.GetPath().Append(L"dir_to_keep");
ASSERT_TRUE(base::CreateDirectory(dir));
base::WriteFile(dir.Append(L"child.txt"), "data");
{
ScopedDirectoryDeleter deleter(dir);
deleter.Release();
}
EXPECT_TRUE(base::DirectoryExists(dir));
EXPECT_TRUE(base::PathExists(dir.Append(L"child.txt")));
}
TEST_F(InstallerFileOpsTest, ScopedDirectoryDeleterDeletesNestedContents) {
base::FilePath dir = temp_dir_.GetPath().Append(L"nested_dir");
base::FilePath sub = dir.Append(L"sub");
ASSERT_TRUE(base::CreateDirectory(sub));
base::WriteFile(sub.Append(L"deep.txt"), "deep");
{
ScopedDirectoryDeleter deleter(dir);
}
EXPECT_FALSE(base::DirectoryExists(dir));
}
TEST_F(InstallerFileOpsTest, ScopedDirectoryDeleterEmptyPathIsNoOp) {
{
ScopedDirectoryDeleter deleter{base::FilePath()};
}
}
TEST_F(InstallerFileOpsTest, ScopedDirectoryDeleterNonexistentPathIsNoOp) {
base::FilePath missing = temp_dir_.GetPath().Append(L"no_such_dir");
ASSERT_FALSE(base::DirectoryExists(missing));
{
ScopedDirectoryDeleter deleter(missing);
}
}
} // namespace
} // namespace cef_installer
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,751 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cef/libcef_dll/bootstrap/installer/installer_launch_state.h"
#include <windows.h>
#include <algorithm>
#include <tuple>
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/no_destructor.h"
#include "base/process/process.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "cef/libcef_dll/bootstrap/installer/installer_constants.h"
#include "cef/libcef_dll/bootstrap/installer/installer_file_integrity.h"
#include "cef/libcef_dll/bootstrap/installer/installer_file_ops.h"
#include "cef/libcef_dll/bootstrap/installer/installer_lock.h"
#include "cef/libcef_dll/bootstrap/installer/installer_paths.h"
namespace cef_installer {
namespace {
uint64_t FileTimeToTicks(FILETIME ft) {
ULARGE_INTEGER li;
li.LowPart = ft.dwLowDateTime;
li.HighPart = ft.dwHighDateTime;
return li.QuadPart;
}
// Process-global active launch state path. See header for the threading
// contract: set once before RunWinMain spawns threads, read-only thereafter.
// Thread creation provides the happens-before edge, so no lock is needed.
base::FilePath& ActiveLaunchStatePath() {
static base::NoDestructor<base::FilePath> path;
return *path;
}
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
base::NoDestructor<RetentionCanonicalObservationHookForTesting>
g_retention_canonical_observation_hook;
std::optional<uint64_t> g_launch_state_gc_time_for_testing;
#endif
uint64_t GetLaunchStateGcCurrentTime() {
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
if (g_launch_state_gc_time_for_testing.has_value()) {
return *g_launch_state_gc_time_for_testing;
}
#endif
FILETIME now = {};
::GetSystemTimePreciseAsFileTime(&now);
return FileTimeToTicks(now);
}
std::optional<base::DictValue> ReadJsonWithIntegrity(
const base::FilePath& path,
IntegrityMismatchAction mismatch_action) {
if (IsReparsePoint(path)) {
return std::nullopt;
}
std::optional<int64_t> size = base::GetFileSize(path);
if (!size || *size > kMaxLaunchStateFileSize) {
return std::nullopt;
}
std::string content;
IntegrityResult integrity = ReadFileWithIntegrity(
path, &content, mismatch_action, kMaxLaunchStateFileSize);
if (integrity != IntegrityResult::kSuccess) {
return std::nullopt;
}
return base::JSONReader::ReadDict(content, base::JSON_PARSE_RFC);
}
std::optional<LivenessRecord> ParseLivenessDict(const base::DictValue& parsed) {
const std::string* appid = parsed.FindString("appid");
const std::string* platform = parsed.FindString("platform");
const std::string* last_launch = parsed.FindString("last_launch");
uint64_t timestamp = 0;
if (!appid || appid->empty() || appid->size() > kMaxUuidLength || !platform ||
platform->empty() || platform->size() > kMaxPlatformLength ||
!last_launch || !base::StringToUint64(*last_launch, &timestamp) ||
timestamp == 0) {
return std::nullopt;
}
return LivenessRecord{*appid, *platform, timestamp};
}
std::optional<LaunchState> ParseLaunchStateDict(const base::DictValue& parsed) {
const std::string* appid = parsed.FindString("appid");
std::optional<int> pid_val = parsed.FindInt("pid");
const std::string* pid_start_str = parsed.FindString("pid_start_time");
const std::string* last_update_str = parsed.FindString("last_update");
std::optional<int> failures = parsed.FindInt("consecutive_failures");
std::optional<bool> running = parsed.FindBool("running");
std::optional<bool> confirmed = parsed.FindBool("confirmed");
if (!appid || !pid_val || !pid_start_str || !last_update_str || !failures ||
!running || !confirmed || appid->size() > kMaxUuidLength) {
return std::nullopt;
}
uint64_t pid_start_time = 0;
uint64_t last_update = 0;
if (!base::StringToUint64(*pid_start_str, &pid_start_time) ||
!base::StringToUint64(*last_update_str, &last_update) ||
last_update == 0) {
return std::nullopt;
}
const std::string* ver = parsed.FindString("version");
const std::string* plat = parsed.FindString("platform");
if (!ver || ver->empty() || ver->size() > kMaxVersionLength || !plat ||
plat->empty() || plat->size() > kMaxPlatformLength) {
return std::nullopt;
}
LaunchState state;
state.appid = *appid;
state.pid = static_cast<uint32_t>(*pid_val);
state.pid_start_time = pid_start_time;
state.consecutive_failures = *failures;
state.running = *running;
state.confirmed = *confirmed;
state.version = *ver;
state.platform = *plat;
state.last_update = last_update;
return state;
}
} // namespace
std::wstring GetAppidHash(const std::string& appid) {
return base::UTF8ToWide(internal::ComputeSha1HexPrefix(appid));
}
bool IsProcessAlive(uint32_t pid, uint64_t pid_start_time) {
base::Process process = base::Process::Open(pid);
if (!process.IsValid()) {
return false;
}
if (!process.IsRunning()) {
return false;
}
return FileTimeToTicks(process.CreationTime().ToFileTime()) == pid_start_time;
}
uint64_t GetCurrentPidStartTime() {
return FileTimeToTicks(base::Process::Current().CreationTime().ToFileTime());
}
uint64_t GetCurrentWallTime() {
FILETIME now = {};
::GetSystemTimePreciseAsFileTime(&now);
return FileTimeToTicks(now);
}
LaunchStateGcAge ClassifyLaunchStateGcAge(uint64_t timestamp) {
if (timestamp == 0) {
return LaunchStateGcAge::kInvalid;
}
const uint64_t now = GetLaunchStateGcCurrentTime();
if (now == 0) {
return LaunchStateGcAge::kUnavailable;
}
if (timestamp > now) {
return LaunchStateGcAge::kFresh;
}
return now - timestamp >= kLaunchStateGcMaxAge ? LaunchStateGcAge::kExpired
: LaunchStateGcAge::kFresh;
}
void SetLaunchStateGcTimeForTesting(std::optional<uint64_t> now) {
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
g_launch_state_gc_time_for_testing = now;
#endif
}
std::optional<uint64_t> GetCurrentBootTime() {
FILETIME now_file_time = {};
::GetSystemTimePreciseAsFileTime(&now_file_time);
const uint64_t now = FileTimeToTicks(now_file_time);
constexpr uint64_t kFileTimeTicksPerMillisecond = 10000;
const uint64_t uptime = ::GetTickCount64();
if (uptime > now / kFileTimeTicksPerMillisecond) {
return std::nullopt;
}
return now - uptime * kFileTimeTicksPerMillisecond;
}
LaunchStateAssessment AssessLaunchState(
LaunchHealthMode mode,
const LaunchState& state,
std::optional<uint64_t> current_boot_time,
std::optional<bool> process_alive) {
LaunchStateAssessment result;
result.consecutive_failures = state.consecutive_failures;
if (mode == LaunchHealthMode::kOff) {
return result;
}
if (!state.running) {
result.status = state.confirmed && state.consecutive_failures == 0
? LaunchStateStatus::kConfirmed
: LaunchStateStatus::kNeutral;
return result;
}
if (!current_boot_time || state.pid_start_time < *current_boot_time ||
!process_alive) {
return result;
}
if (*process_alive) {
result.status = LaunchStateStatus::kRunning;
return result;
}
result.status = LaunchStateStatus::kFailure;
result.consecutive_failures = state.consecutive_failures + 1;
return result;
}
bool ApplyLaunchExit(LaunchHealthMode mode,
bool normal_exit,
bool neutral_exit,
LaunchState* state) {
if (!state || mode == LaunchHealthMode::kOff || !state->running) {
return false;
}
if (mode == LaunchHealthMode::kExplicit) {
state->running = false;
state->confirmed = false;
return true;
}
if (normal_exit) {
state->consecutive_failures = 0;
state->running = false;
state->confirmed = true;
return true;
}
if (neutral_exit) {
state->running = false;
state->confirmed = false;
return true;
}
return false;
}
AppLaunchHealthEvaluation EvaluateAppLaunchHealth(
const base::FilePath& install_dir,
const std::string& appid,
LaunchHealthMode mode,
int failure_threshold) {
AppLaunchHealthEvaluation result;
if (mode == LaunchHealthMode::kOff || install_dir.empty() || appid.empty()) {
return result;
}
result.states = ScanLaunchStates(install_dir, GetAppidHash(appid));
const std::optional<uint64_t> boot_time = GetCurrentBootTime();
FILETIME now_file_time = {};
::GetSystemTimePreciseAsFileTime(&now_file_time);
const uint64_t now = FileTimeToTicks(now_file_time);
for (const auto& [version, state] : result.states) {
std::optional<bool> process_alive;
if (boot_time && state.pid_start_time >= *boot_time &&
state.pid_start_time <= now) {
process_alive = IsProcessAlive(state.pid, state.pid_start_time);
}
LaunchStateAssessment assessment =
AssessLaunchState(mode, state, boot_time, process_alive);
result.assessments.emplace(version, assessment);
if (assessment.status == LaunchStateStatus::kFailure &&
assessment.consecutive_failures >= failure_threshold) {
result.disqualified_versions.insert(version);
}
}
return result;
}
void SetActiveLaunchStatePath(const base::FilePath& path) {
// Set-once contract: called during single-threaded startup before
// RunWinMain spawns any threads. Never mutate after startup.
ActiveLaunchStatePath() = path;
}
base::FilePath GetActiveLaunchStatePath() {
return ActiveLaunchStatePath();
}
// ============================================================================
// Install-dir-level API (.launch/ directory)
// ============================================================================
base::FilePath GetLaunchStateDir(const base::FilePath& install_dir) {
return install_dir.Append(kLaunchStateDirName);
}
base::FilePath GetInstallDirLaunchStatePath(const base::FilePath& install_dir,
const std::wstring& appid_hash,
const Version& version,
const std::string& platform) {
std::wstring filename = appid_hash + L"_" +
base::UTF8ToWide(version.ToString()) + L"_" +
base::UTF8ToWide(platform);
return GetLaunchStateDir(install_dir).Append(filename);
}
base::FilePath GetInstallDirLivenessPath(const base::FilePath& install_dir,
const std::wstring& appid_hash,
const std::string& platform) {
std::wstring filename = appid_hash + L"_" + base::UTF8ToWide(platform);
return GetLaunchStateDir(install_dir).Append(filename);
}
std::optional<LivenessRecord> ReadLivenessPath(
const base::FilePath& path,
IntegrityMismatchAction mismatch_action) {
std::optional<base::DictValue> parsed =
ReadJsonWithIntegrity(path, mismatch_action);
if (!parsed) {
return std::nullopt;
}
return ParseLivenessDict(*parsed);
}
namespace {
bool WriteLivenessPathImpl(const base::FilePath& path,
const LivenessRecord& record) {
if (record.appid.empty() || record.appid.size() > kMaxUuidLength ||
record.platform.empty() || record.platform.size() > kMaxPlatformLength) {
return false;
}
if (record.last_launch == 0) {
return false;
}
base::FilePath dir = path.DirName();
if (IsReparsePoint(dir) ||
(!base::DirectoryExists(dir) &&
(!VerifySafeDirectoryPath(dir) || !base::CreateDirectory(dir))) ||
!VerifySafeFilePath(path)) {
return false;
}
base::DictValue dict;
dict.Set("appid", record.appid);
dict.Set("platform", record.platform);
dict.Set("last_launch", base::NumberToString(record.last_launch));
std::string json;
return base::JSONWriter::Write(dict, &json) &&
WriteFileWithIntegrity(path, json);
}
} // namespace
bool WriteLivenessPath(const base::FilePath& path,
const LivenessRecord& record) {
return WriteLivenessPathImpl(path, record);
}
bool RefreshLivenessPath(const base::FilePath& path,
const std::string& appid,
const std::string& platform,
uint64_t now) {
if (now == 0) {
return false;
}
std::optional<LivenessRecord> current = ReadLivenessPath(path);
if (current && current->appid == appid && current->platform == platform &&
current->last_launch <= now &&
now - current->last_launch < kLivenessRefreshIntervalFileTimeTicks) {
return true;
}
return WriteLivenessPathImpl(path, LivenessRecord{appid, platform, now});
}
std::optional<LaunchState> ReadLaunchStatePath(
const base::FilePath& path,
IntegrityMismatchAction mismatch_action) {
if (IsReparsePoint(path)) {
return std::nullopt;
}
std::optional<base::DictValue> parsed =
ReadJsonWithIntegrity(path, mismatch_action);
if (!parsed) {
return std::nullopt;
}
return ParseLaunchStateDict(*parsed);
}
std::optional<LaunchRecordSnapshot> ReadLaunchRecordSnapshot(
const base::FilePath& path,
const base::FilePath& expected_resolved_parent) {
if (expected_resolved_parent.empty()) {
return std::nullopt;
}
if (IsReparsePoint(path)) {
LaunchRecordSnapshot snapshot;
snapshot.path = path;
snapshot.resolved_parent_path = expected_resolved_parent;
snapshot.reparse_point = true;
return snapshot;
}
std::string content;
std::string raw_content;
base::FilePath resolved_path;
bool too_large = false;
IntegrityResult integrity = ReadFileWithIntegrity(
path, &content, IntegrityMismatchAction::kPreserve,
kMaxLaunchStateFileSize, &resolved_path, &raw_content, &too_large);
const bool bounded_raw_invalid =
integrity == IntegrityResult::kReadError && !raw_content.empty();
if ((!too_large && !bounded_raw_invalid &&
integrity != IntegrityResult::kSuccess &&
integrity != IntegrityResult::kSuccessNoFooter &&
integrity != IntegrityResult::kIntegrityMismatch) ||
resolved_path.empty() ||
!base::FilePath::CompareEqualIgnoreCase(
resolved_path.DirName().value(), expected_resolved_parent.value())) {
return std::nullopt;
}
LaunchRecordSnapshot snapshot;
snapshot.path = path;
snapshot.resolved_parent_path = expected_resolved_parent;
snapshot.content = std::move(content);
snapshot.raw_content = std::move(raw_content);
snapshot.oversized = too_large;
if (too_large) {
return snapshot;
}
if (integrity != IntegrityResult::kSuccess) {
return snapshot;
}
std::optional<base::DictValue> parsed =
base::JSONReader::ReadDict(snapshot.content, base::JSON_PARSE_RFC);
if (!parsed) {
return snapshot;
}
if (std::optional<LaunchState> health = ParseLaunchStateDict(*parsed)) {
snapshot.kind = LaunchRecordKind::kHealth;
snapshot.health = std::move(*health);
return snapshot;
}
if (std::optional<LivenessRecord> liveness = ParseLivenessDict(*parsed)) {
snapshot.kind = LaunchRecordKind::kLiveness;
snapshot.liveness = std::move(*liveness);
return snapshot;
}
return snapshot;
}
RetentionEvidenceSnapshot CollectRetentionEvidenceSnapshot(
const base::FilePath& install_dir,
const std::vector<AppEntry>& registrations) {
RetentionEvidenceSnapshot snapshot;
RetentionEvidenceMap& result = snapshot.evidence;
for (const auto& app : registrations) {
result[{app.uuid, app.platform}] = {};
}
base::FilePath launch_dir = GetLaunchStateDir(install_dir);
if (!base::DirectoryExists(launch_dir)) {
return snapshot;
}
if (IsReparsePoint(launch_dir)) {
for (auto& [key, evidence] : result) {
evidence.unknown = true;
evidence.diagnostic = "unsafe_launch_directory";
}
return snapshot;
}
std::optional<base::FilePath> resolved_launch_dir =
GetSafeDirectoryResolvedPath(launch_dir);
if (!resolved_launch_dir) {
for (auto& [key, evidence] : result) {
evidence.unknown = true;
evidence.diagnostic = "unsafe_launch_directory";
}
return snapshot;
}
std::set<base::FilePath> canonical_liveness_paths;
auto observe_path = [&](const base::FilePath& path) {
if (!base::PathExists(path)) {
return;
}
const std::wstring filename = path.BaseName().value();
const bool reparse = IsReparsePoint(path);
std::string content;
base::FilePath resolved_path;
IntegrityResult integrity = IntegrityResult::kReadError;
if (!reparse) {
std::optional<int64_t> size = base::GetFileSize(path);
if (size && *size <= kMaxLaunchStateFileSize) {
integrity = ReadFileWithIntegrity(
path, &content, IntegrityMismatchAction::kPreserve,
kMaxLaunchStateFileSize, &resolved_path);
}
}
const bool resolved_parent_matches =
!resolved_path.empty() &&
base::FilePath::CompareEqualIgnoreCase(resolved_path.DirName().value(),
resolved_launch_dir->value());
const bool readable =
resolved_parent_matches && integrity == IntegrityResult::kSuccess;
std::optional<base::DictValue> parsed =
readable ? base::JSONReader::ReadDict(content, base::JSON_PARSE_RFC)
: std::nullopt;
std::optional<LivenessRecord> liveness =
parsed ? ParseLivenessDict(*parsed) : std::nullopt;
std::optional<LaunchState> health =
parsed ? ParseLaunchStateDict(*parsed) : std::nullopt;
for (const auto& app : registrations) {
const std::wstring hash = GetAppidHash(app.uuid);
const std::wstring prefix = hash + L"_";
const std::wstring platform = base::UTF8ToWide(app.platform);
const base::FilePath canonical_liveness =
GetInstallDirLivenessPath(install_dir, hash, app.platform);
const bool potential =
path == canonical_liveness ||
(filename.starts_with(prefix) && filename.ends_with(L"_" + platform));
const bool liveness_identity = liveness && liveness->appid == app.uuid &&
liveness->platform == app.platform;
const bool health_identity = health && health->appid == app.uuid &&
health->platform == app.platform;
if (!potential && !liveness_identity && !health_identity) {
continue;
}
RetentionEvidence& evidence = result[{app.uuid, app.platform}];
if (reparse) {
evidence.unknown = true;
evidence.diagnostic = "invalid_or_noncanonical_evidence";
continue;
}
uint64_t timestamp = 0;
RetentionEvidenceKind kind = RetentionEvidenceKind::kNone;
bool canonical = false;
if (liveness_identity) {
timestamp = liveness->last_launch;
kind = RetentionEvidenceKind::kLiveness;
canonical = path == canonical_liveness;
} else if (health_identity) {
Version version = Version::Parse(health->version);
if (version.IsValid()) {
canonical = path == GetInstallDirLaunchStatePath(
install_dir, hash, version, app.platform);
}
timestamp = health->pid_start_time;
kind = RetentionEvidenceKind::kHealthSentinel;
}
if (!canonical || kind == RetentionEvidenceKind::kNone) {
evidence.unknown = true;
evidence.diagnostic = "invalid_or_noncanonical_evidence";
continue;
}
if (readable) {
snapshot.files.push_back({{app.uuid, app.platform},
path,
*resolved_launch_dir,
content,
integrity == IntegrityResult::kSuccess});
}
if (timestamp > evidence.timestamp) {
evidence.timestamp = timestamp;
evidence.kind = kind;
}
}
};
// Read each registration's canonical liveness path directly. This read is
// that registration's launch-intent ordering point; a later directory scan
// must not reinterpret a replacement as pre-cutoff evidence.
for (const auto& app : registrations) {
base::FilePath path = GetInstallDirLivenessPath(
install_dir, GetAppidHash(app.uuid), app.platform);
canonical_liveness_paths.insert(path);
observe_path(path);
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
if (*g_retention_canonical_observation_hook) {
g_retention_canonical_observation_hook->Run(path);
}
#endif
}
base::FileEnumerator enumerator(launch_dir, /*recursive=*/false,
base::FileEnumerator::FILES);
for (base::FilePath path = enumerator.Next(); !path.empty();
path = enumerator.Next()) {
if (!canonical_liveness_paths.contains(path)) {
observe_path(path);
}
}
std::sort(snapshot.files.begin(), snapshot.files.end(),
[](const auto& a, const auto& b) {
return std::tie(a.key, a.path) < std::tie(b.key, b.path);
});
return snapshot;
}
void SetRetentionCanonicalObservationHookForTesting(
RetentionCanonicalObservationHookForTesting callback) {
#if !(defined(OFFICIAL_BUILD) && defined(NDEBUG))
*g_retention_canonical_observation_hook = std::move(callback);
#endif
}
RetentionEvidenceMap CollectRetentionEvidence(
const base::FilePath& install_dir,
const std::vector<AppEntry>& registrations) {
return CollectRetentionEvidenceSnapshot(install_dir, registrations).evidence;
}
std::set<VersionKey> CollectConfirmedVersionProtection(
const base::FilePath& install_dir,
const std::vector<AppEntry>& registrations) {
std::set<VersionKey> result;
std::set<RetentionRegistrationKey> registered;
for (const auto& app : registrations) {
registered.insert({app.uuid, app.platform});
}
base::FilePath launch_dir = GetLaunchStateDir(install_dir);
if (!base::DirectoryExists(launch_dir) || IsReparsePoint(launch_dir)) {
return result;
}
base::FileEnumerator enumerator(launch_dir, /*recursive=*/false,
base::FileEnumerator::FILES);
for (base::FilePath path = enumerator.Next(); !path.empty();
path = enumerator.Next()) {
if (IsReparsePoint(path)) {
continue;
}
std::optional<LaunchState> health =
ReadLaunchStatePath(path, IntegrityMismatchAction::kPreserve);
if (!health || health->running || !health->confirmed ||
health->consecutive_failures != 0 ||
!registered.contains({health->appid, health->platform})) {
continue;
}
Version version = Version::Parse(health->version);
if (!version.IsValid() ||
path != GetInstallDirLaunchStatePath(install_dir,
GetAppidHash(health->appid),
version, health->platform)) {
continue;
}
result.insert({version, health->platform});
}
return result;
}
namespace {
bool WriteLaunchStatePathImpl(const base::FilePath& path,
const LaunchState& state) {
const uint64_t last_update = GetLaunchStateGcCurrentTime();
if (last_update == 0) {
return false;
}
base::FilePath dir = path.DirName();
if (IsReparsePoint(dir)) {
return false;
}
if (!base::DirectoryExists(dir)) {
if (!VerifySafeDirectoryPath(dir)) {
return false;
}
if (!base::CreateDirectory(dir)) {
return false;
}
}
if (!VerifySafeFilePath(path)) {
return false;
}
if (state.appid.empty() || state.version.empty() || state.platform.empty()) {
return false;
}
base::DictValue dict;
dict.Set("appid", state.appid);
dict.Set("pid", static_cast<int>(state.pid));
dict.Set("pid_start_time", base::NumberToString(state.pid_start_time));
dict.Set("consecutive_failures", state.consecutive_failures);
dict.Set("running", state.running);
dict.Set("confirmed", state.confirmed);
dict.Set("version", state.version);
dict.Set("platform", state.platform);
dict.Set("last_update", base::NumberToString(last_update));
std::string json;
if (!base::JSONWriter::Write(dict, &json)) {
return false;
}
return WriteFileWithIntegrity(path, json);
}
} // namespace
bool WriteLaunchStatePath(const base::FilePath& path,
const LaunchState& state) {
return WriteLaunchStatePathImpl(path, state);
}
std::map<Version, LaunchState> ScanLaunchStates(
const base::FilePath& install_dir,
const std::wstring& appid_hash) {
std::map<Version, LaunchState> result;
base::FilePath launch_dir = GetLaunchStateDir(install_dir);
if (!base::DirectoryExists(launch_dir)) {
return result;
}
if (IsReparsePoint(launch_dir)) {
return result;
}
std::string current_platform = GetCurrentPlatform();
base::FileEnumerator enumerator(launch_dir, /*recursive=*/false,
base::FileEnumerator::FILES,
appid_hash + L"_*");
for (base::FilePath path = enumerator.Next(); !path.empty();
path = enumerator.Next()) {
auto ls = ReadLaunchStatePath(path);
if (!ls) {
continue;
}
if (ls->platform != current_platform) {
continue;
}
Version ver = Version::Parse(ls->version);
if (!ver.IsValid()) {
continue;
}
result.emplace(ver, *ls);
}
return result;
}
} // namespace cef_installer
@@ -0,0 +1,290 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_LAUNCH_STATE_H_
#define CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_LAUNCH_STATE_H_
#include <cstdint>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <vector>
#include "base/files/file_path.h"
#include "base/functional/callback_forward.h"
#include "cef/libcef_dll/bootstrap/installer/installer_config.h"
#include "cef/libcef_dll/bootstrap/installer/installer_retention.h"
#include "cef/libcef_dll/bootstrap/installer/installer_version.h"
namespace cef_installer {
struct LaunchState {
std::string appid;
uint32_t pid = 0;
uint64_t pid_start_time = 0;
int consecutive_failures = 0;
bool running = false;
// True only after explicit confirmation or a successful exit-code-mode
// launch.
bool confirmed = false;
std::string version;
std::string platform;
// Writer-owned publication time used only for launch-record garbage
// collection. Callers cannot preserve or choose this value.
uint64_t last_update = 0;
};
struct LivenessRecord {
std::string appid;
std::string platform;
uint64_t last_launch = 0;
};
enum class LaunchRecordKind {
kInvalid,
kHealth,
kLiveness,
};
// One bounded, no-follow observation of a launch record. Valid records expose
// their integrity-verified payload; all conclusive observations expose the
// exact raw publication for race-safe repair.
struct LaunchRecordSnapshot {
LaunchRecordKind kind = LaunchRecordKind::kInvalid;
LaunchState health;
LivenessRecord liveness;
base::FilePath path;
base::FilePath resolved_parent_path;
std::string content;
std::string raw_content;
bool oversized = false;
bool reparse_point = false;
};
enum class LaunchStateGcAge {
kInvalid,
kUnavailable,
kFresh,
kExpired,
};
// Exact canonical evidence file observed while building a retention plan.
// |content| is the integrity-verified payload without the integrity footer.
// |resolved_parent_path| binds later compare-before-delete cleanup to the
// observed directory location after logical publication.
struct RetentionEvidenceFileSnapshot {
RetentionRegistrationKey key;
base::FilePath path;
base::FilePath resolved_parent_path;
std::string content;
bool integrity_protected = false;
};
struct RetentionEvidenceSnapshot {
RetentionEvidenceMap evidence;
std::vector<RetentionEvidenceFileSnapshot> files;
};
enum class LaunchStateStatus {
kConfirmed,
kNeutral,
kRunning,
kFailure,
kIndeterminate,
};
struct LaunchStateAssessment {
LaunchStateStatus status = LaunchStateStatus::kIndeterminate;
int consecutive_failures = 0;
};
// Selection-policy-free evaluation for one app and the current platform.
// Off mode returns an empty result without enumerating .launch/. Callers pass
// only |disqualified_versions| to the shared selector and may use |states| and
// |assessments| to seed the selected version's next sentinel.
struct AppLaunchHealthEvaluation {
std::map<Version, LaunchState> states;
std::map<Version, LaunchStateAssessment> assessments;
std::set<Version> disqualified_versions;
};
// Estimate the current Windows boot boundary in FILETIME ticks using the
// system wall clock minus GetTickCount64(). Returns nullopt if the conversion
// is internally inconsistent. Clock changes can make the estimate conservative
// (indeterminate) but never turn a known pre-boot record into a confirmed run.
std::optional<uint64_t> GetCurrentBootTime();
// Pure classification for an existing record. |process_alive| is consulted
// only for a running record from the current boot. A missing boot boundary is
// indeterminate, and a start time before the boundary never counts as failure.
LaunchStateAssessment AssessLaunchState(
LaunchHealthMode mode,
const LaunchState& state,
std::optional<uint64_t> current_boot_time,
std::optional<bool> process_alive);
// Apply a completed RunWinMain return to |state|. Returns true when the state
// should be persisted. Explicit mode treats every ordinary return as neutral;
// exit-code mode confirms zero, preserves neutral codes, and leaves abnormal
// exits running for failure classification on the next launch.
bool ApplyLaunchExit(LaunchHealthMode mode,
bool normal_exit,
bool neutral_exit,
LaunchState* state);
AppLaunchHealthEvaluation EvaluateAppLaunchHealth(
const base::FilePath& install_dir,
const std::string& appid,
LaunchHealthMode mode,
int failure_threshold);
// Key for identifying a confirmed version (version + platform).
// Defined here to avoid depending on VersionKey from
// installer_version_resolver.
struct ConfirmedVersionKey {
Version version;
std::string platform;
bool operator<(const ConfirmedVersionKey& other) const {
if (version != other.version) {
return version < other.version;
}
return platform < other.platform;
}
bool operator==(const ConfirmedVersionKey& other) const {
return version == other.version && platform == other.platform;
}
};
// Compute the appid hash: first 16 hex chars of SHA-1(appid).
std::wstring GetAppidHash(const std::string& appid);
// Check if a process with the given PID and creation time is still alive.
// Uses OpenProcess + GetProcessTimes to guard against PID reuse.
bool IsProcessAlive(uint32_t pid, uint64_t pid_start_time);
// Return the creation time of the current process as a uint64 FILETIME.
uint64_t GetCurrentPidStartTime();
uint64_t GetCurrentWallTime();
// Classify a nonzero CRC-verified content timestamp against the dedicated
// launch-GC clock. Future timestamps are fresh and unavailable time preserves
// the record.
LaunchStateGcAge ClassifyLaunchStateGcAge(uint64_t timestamp);
// Process-global test seam shared only by launch publication and launch GC.
// Passing nullopt restores the production wall clock. Tests must reset it.
void SetLaunchStateGcTimeForTesting(std::optional<uint64_t> now);
// Returns install_dir/.launch/.
base::FilePath GetLaunchStateDir(const base::FilePath& install_dir);
// Returns install_dir/.launch/<appid_hash>_<version>_<platform>.
base::FilePath GetInstallDirLaunchStatePath(const base::FilePath& install_dir,
const std::wstring& appid_hash,
const Version& version,
const std::string& platform);
// Returns install_dir/.launch/<appid_hash>_<platform>. The version-less
// two-segment shape cannot collide with a health sentinel.
base::FilePath GetInstallDirLivenessPath(const base::FilePath& install_dir,
const std::wstring& appid_hash,
const std::string& platform);
// Read a current-schema liveness record with a valid integrity footer and a
// nonzero last_launch. The default preserves integrity mismatches.
std::optional<LivenessRecord> ReadLivenessPath(
const base::FilePath& path,
IntegrityMismatchAction mismatch_action =
IntegrityMismatchAction::kPreserve);
// Write an integrity-protected liveness record. Zero last_launch is rejected.
bool WriteLivenessPath(const base::FilePath& path,
const LivenessRecord& record);
// Refresh only when missing/malformed or at least 30 days old. Future content
// timestamps are treated as fresh so wall-clock rollback does not cause a
// write on every launch.
bool RefreshLivenessPath(const base::FilePath& path,
const std::string& appid,
const std::string& platform,
uint64_t now);
// Read a current-schema launch state from a full path (install-dir-level
// format). A valid integrity footer and nonzero last_update are required.
// Includes version and platform fields from the JSON body. The default
// preserves an integrity mismatch for read-only callers.
std::optional<LaunchState> ReadLaunchStatePath(
const base::FilePath& path,
IntegrityMismatchAction mismatch_action =
IntegrityMismatchAction::kPreserve);
// Observe exactly one bounded publication. Returns kInvalid for conclusively
// corrupt, footerless, schema-invalid, or final-component reparse publication
// so a writer can conditionally repair that exact publication. I/O and
// path-identity uncertainty returns nullopt. The resolved parent must match
// |expected_resolved_parent|.
std::optional<LaunchRecordSnapshot> ReadLaunchRecordSnapshot(
const base::FilePath& path,
const base::FilePath& expected_resolved_parent);
// Collect the newest canonical health-sentinel pid_start_time or liveness-only
// last_launch for each durable registration. This is a strictly read-only
// retention snapshot: it never repairs, deletes, creates, or rewrites files.
// A malformed, reparse, mismatched, or noncanonical file that could belong to
// a registration marks that registration's evidence unknown/protected.
RetentionEvidenceMap CollectRetentionEvidence(
const base::FilePath& install_dir,
const std::vector<AppEntry>& registrations);
// Collect evidence plus the exact canonical file payloads that produced it.
// This is side-effect-free and is used by apply for final validation and
// compare-before-delete cleanup.
RetentionEvidenceSnapshot CollectRetentionEvidenceSnapshot(
const base::FilePath& install_dir,
const std::vector<AppEntry>& registrations);
using RetentionCanonicalObservationHookForTesting =
base::RepeatingCallback<void(const base::FilePath&)>;
void SetRetentionCanonicalObservationHookForTesting(
RetentionCanonicalObservationHookForTesting callback);
// Side-effect-free collection of canonical, confirmed health sentinels for
// registrations that remain in the supplied durable database snapshot.
std::set<VersionKey> CollectConfirmedVersionProtection(
const base::FilePath& install_dir,
const std::vector<AppEntry>& registrations);
// Write a launch state file to a full path (install-dir-level format).
// Creates the .launch/ directory if it doesn't exist and replaces any
// caller-supplied last_update with the current launch-GC clock.
bool WriteLaunchStatePath(const base::FilePath& path, const LaunchState& state);
// Process-global active launch state path.
//
// Set once by the bootstrap before RunWinMain (during single-threaded
// startup), and read thereafter by HandleLaunchSuccess (e.g. on a background
// task) to resolve the sentinel file for the current launch. No lock is
// needed: the set-once-before-threads contract gives the happens-before edge.
// Set the active launch state path. Called exactly once by the bootstrap
// before RunWinMain spawns any threads.
void SetActiveLaunchStatePath(const base::FilePath& path);
// Return the active launch state path, or an empty path if never set (e.g.
// not running under the bootstrap, or a bundled/unchecked path that does not
// participate in launch health).
base::FilePath GetActiveLaunchStatePath();
// Scan .launch/<appid_hash>_* files for a specific appid.
// Filters to the current platform. Returns map keyed by version.
// Returns empty map if .launch/ doesn't exist.
std::map<Version, LaunchState> ScanLaunchStates(
const base::FilePath& install_dir,
const std::wstring& appid_hash);
} // namespace cef_installer
#endif // CEF_LIBCEF_DLL_BOOTSTRAP_INSTALLER_INSTALLER_LAUNCH_STATE_H_
@@ -0,0 +1,837 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cef/libcef_dll/bootstrap/installer/installer_launch_state.h"
#include <windows.h>
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "cef/libcef_dll/bootstrap/installer/installer_constants.h"
#include "cef/libcef_dll/bootstrap/installer/installer_file_integrity.h"
#include "cef/libcef_dll/bootstrap/installer/installer_lock.h"
#include "cef/libcef_dll/bootstrap/installer/installer_paths.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace cef_installer {
class LaunchStateTest : public testing::Test {
protected:
void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); }
void TearDown() override {
SetRetentionCanonicalObservationHookForTesting({});
SetLaunchStateGcTimeForTesting(std::nullopt);
}
base::FilePath install_dir() { return temp_dir_.GetPath(); }
base::ScopedTempDir temp_dir_;
};
TEST_F(LaunchStateTest, GetAppidHash_Deterministic) {
std::string appid = "A3B9C4D5-E6F7-4A8B-9C0D-E1F2A3B4C5D6";
std::wstring hash1 = GetAppidHash(appid);
std::wstring hash2 = GetAppidHash(appid);
EXPECT_EQ(hash1, hash2);
EXPECT_EQ(16u, hash1.size());
}
TEST_F(LaunchStateTest, GetAppidHash_DifferentAppids) {
std::wstring hash1 = GetAppidHash("A3B9C4D5-E6F7-4A8B-9C0D-E1F2A3B4C5D6");
std::wstring hash2 = GetAppidHash("B4C0D5E6-F7A8-5B9C-0D1E-F2A3B4C5D6E7");
EXPECT_NE(hash1, hash2);
}
TEST_F(LaunchStateTest, IsProcessAlive_CurrentProcess) {
uint32_t pid = GetCurrentProcessId();
uint64_t start_time = GetCurrentPidStartTime();
EXPECT_TRUE(IsProcessAlive(pid, start_time));
}
TEST_F(LaunchStateTest, IsProcessAlive_DeadProcess) {
EXPECT_FALSE(IsProcessAlive(99999, 1));
}
TEST_F(LaunchStateTest, IsProcessAlive_WrongStartTime) {
uint32_t pid = GetCurrentProcessId();
EXPECT_FALSE(IsProcessAlive(pid, 1));
}
TEST_F(LaunchStateTest, AssessLaunchStateModeAndBootMatrix) {
LaunchState state;
state.running = true;
state.pid_start_time = 200;
state.consecutive_failures = 2;
EXPECT_EQ(
LaunchStateStatus::kIndeterminate,
AssessLaunchState(LaunchHealthMode::kOff, state, 100, false).status);
EXPECT_EQ(LaunchStateStatus::kIndeterminate,
AssessLaunchState(LaunchHealthMode::kExplicit, state, std::nullopt,
std::nullopt)
.status);
EXPECT_EQ(
LaunchStateStatus::kIndeterminate,
AssessLaunchState(LaunchHealthMode::kExitCode, state, 201, std::nullopt)
.status);
EXPECT_EQ(
LaunchStateStatus::kRunning,
AssessLaunchState(LaunchHealthMode::kExplicit, state, 200, true).status);
const LaunchStateAssessment failure =
AssessLaunchState(LaunchHealthMode::kExplicit, state, 200, false);
EXPECT_EQ(LaunchStateStatus::kFailure, failure.status);
EXPECT_EQ(3, failure.consecutive_failures);
}
TEST_F(LaunchStateTest, AssessLaunchStateConfirmedAndNeutral) {
LaunchState state;
state.running = false;
state.consecutive_failures = 0;
state.confirmed = true;
EXPECT_EQ(LaunchStateStatus::kConfirmed,
AssessLaunchState(LaunchHealthMode::kExplicit, state, std::nullopt,
std::nullopt)
.status);
state.consecutive_failures = 2;
EXPECT_EQ(LaunchStateStatus::kNeutral,
AssessLaunchState(LaunchHealthMode::kExitCode, state, std::nullopt,
std::nullopt)
.status);
}
TEST_F(LaunchStateTest, ApplyLaunchExitModeMatrix) {
LaunchState state;
state.running = true;
state.consecutive_failures = 2;
EXPECT_FALSE(ApplyLaunchExit(LaunchHealthMode::kOff, true, false, &state));
EXPECT_TRUE(state.running);
EXPECT_TRUE(
ApplyLaunchExit(LaunchHealthMode::kExplicit, false, false, &state));
EXPECT_FALSE(state.running);
EXPECT_EQ(2, state.consecutive_failures);
state.running = true;
EXPECT_TRUE(
ApplyLaunchExit(LaunchHealthMode::kExitCode, true, false, &state));
EXPECT_FALSE(state.running);
EXPECT_EQ(0, state.consecutive_failures);
state.running = true;
state.consecutive_failures = 2;
EXPECT_TRUE(
ApplyLaunchExit(LaunchHealthMode::kExitCode, false, true, &state));
EXPECT_FALSE(state.running);
EXPECT_EQ(2, state.consecutive_failures);
state.running = true;
EXPECT_FALSE(
ApplyLaunchExit(LaunchHealthMode::kExitCode, false, false, &state));
EXPECT_TRUE(state.running);
}
TEST_F(LaunchStateTest,
EvaluateAppLaunchHealthOffSkipsAndExplicitDisqualifies) {
const std::string appid = "550e8400-e29b-41d4-a716-446655440000";
const Version version = Version::Parse("150.1.0");
LaunchState state;
state.appid = appid;
state.pid = 99999;
state.pid_start_time = GetCurrentPidStartTime();
state.consecutive_failures = 2;
state.running = true;
state.confirmed = false;
state.version = version.ToString();
state.platform = GetCurrentPlatform();
ASSERT_TRUE(WriteLaunchStatePath(
GetInstallDirLaunchStatePath(install_dir(), GetAppidHash(appid), version,
GetCurrentPlatform()),
state));
AppLaunchHealthEvaluation off =
EvaluateAppLaunchHealth(install_dir(), appid, LaunchHealthMode::kOff, 3);
EXPECT_TRUE(off.states.empty());
EXPECT_TRUE(off.disqualified_versions.empty());
AppLaunchHealthEvaluation explicit_mode = EvaluateAppLaunchHealth(
install_dir(), appid, LaunchHealthMode::kExplicit, 3);
EXPECT_EQ(1u, explicit_mode.states.size());
EXPECT_EQ(1u, explicit_mode.disqualified_versions.count(version));
}
TEST_F(LaunchStateTest, MissingConfirmedFieldIsRejected) {
base::FilePath path =
GetLaunchStateDir(install_dir()).AppendASCII("legacy_state");
ASSERT_TRUE(base::CreateDirectory(path.DirName()));
ASSERT_TRUE(
base::WriteFile(path, R"({"appid":"legacy","pid":1,"pid_start_time":"1",)"
R"("consecutive_failures":0,"running":false,)"
R"("version":"150.1.0","platform":"windows64"})"));
std::optional<LaunchState> state = ReadLaunchStatePath(path);
EXPECT_FALSE(state);
}
// ============================================================================
// Process-global active launch state path
// ============================================================================
TEST_F(LaunchStateTest, ActiveLaunchStatePath_DefaultsEmpty) {
// Defaults to empty before any set. RoundTrip restores empty so this holds
// regardless of test execution order.
EXPECT_TRUE(GetActiveLaunchStatePath().empty());
}
TEST_F(LaunchStateTest, ActiveLaunchStatePath_RoundTrip) {
base::FilePath path =
install_dir().Append(L".launch").Append(L"abc_150.1.0_windows64");
SetActiveLaunchStatePath(path);
EXPECT_EQ(path, GetActiveLaunchStatePath());
// Restore the empty default so other tests (and DefaultsEmpty) see a clean
// process-global.
SetActiveLaunchStatePath(base::FilePath());
EXPECT_TRUE(GetActiveLaunchStatePath().empty());
}
// ============================================================================
// Install-dir-level API tests (.launch/ directory)
// ============================================================================
TEST_F(LaunchStateTest, GetLaunchStateDir_ReturnsCorrectPath) {
base::FilePath dir = GetLaunchStateDir(install_dir());
EXPECT_EQ(dir, install_dir().Append(L".launch"));
}
TEST_F(LaunchStateTest, GetInstallDirLaunchStatePath_Format) {
Version ver = Version::Parse("150.1.0");
base::FilePath path = GetInstallDirLaunchStatePath(
install_dir(), L"abc123def4567890", ver, "windows64");
EXPECT_EQ(path, install_dir()
.Append(L".launch")
.Append(L"abc123def4567890_150.1.0_windows64"));
}
TEST_F(LaunchStateTest, ReadWriteLaunchStatePath_RoundTrip) {
base::FilePath launch_dir = install_dir().Append(L".launch");
base::FilePath path =
launch_dir.Append(L"abc123def4567890_150.1.0_windows64");
LaunchState state;
state.appid = "test-app-uuid";
state.pid = 42;
state.pid_start_time = 133612345678901234ULL;
state.consecutive_failures = 1;
state.running = true;
state.version = "150.1.0";
state.platform = "windows64";
constexpr uint64_t kPublicationTime = 123456789;
SetLaunchStateGcTimeForTesting(kPublicationTime);
state.last_update = 1;
ASSERT_TRUE(WriteLaunchStatePath(path, state));
auto read = ReadLaunchStatePath(path);
ASSERT_TRUE(read.has_value());
EXPECT_EQ(state.appid, read->appid);
EXPECT_EQ(state.pid, read->pid);
EXPECT_EQ(state.pid_start_time, read->pid_start_time);
EXPECT_EQ(state.consecutive_failures, read->consecutive_failures);
EXPECT_EQ(state.running, read->running);
EXPECT_EQ(state.version, read->version);
EXPECT_EQ(state.platform, read->platform);
EXPECT_EQ(kPublicationTime, read->last_update);
}
TEST_F(LaunchStateTest, HealthWriterOwnsAndRefreshesLastUpdate) {
base::FilePath path = GetInstallDirLaunchStatePath(
install_dir(), L"abc123def4567890", Version::Parse("150.1.0"),
GetCurrentPlatform());
LaunchState state;
state.appid = "test-app";
state.pid_start_time = 1;
state.version = "150.1.0";
state.platform = GetCurrentPlatform();
state.last_update = 7;
SetLaunchStateGcTimeForTesting(100);
ASSERT_TRUE(WriteLaunchStatePath(path, state));
ASSERT_EQ(100u, ReadLaunchStatePath(path)->last_update);
SetLaunchStateGcTimeForTesting(200);
ASSERT_TRUE(WriteLaunchStatePath(path, state));
EXPECT_EQ(200u, ReadLaunchStatePath(path)->last_update);
}
TEST_F(LaunchStateTest, ZeroPublicationClockPreservesPriorHealthState) {
base::FilePath path = GetInstallDirLaunchStatePath(
install_dir(), L"abc123def4567890", Version::Parse("150.1.0"),
GetCurrentPlatform());
LaunchState state;
state.appid = "test-app";
state.pid_start_time = 1;
state.version = "150.1.0";
state.platform = GetCurrentPlatform();
SetLaunchStateGcTimeForTesting(100);
ASSERT_TRUE(WriteLaunchStatePath(path, state));
std::string before;
ASSERT_TRUE(base::ReadFileToString(path, &before));
SetLaunchStateGcTimeForTesting(0);
EXPECT_FALSE(WriteLaunchStatePath(path, state));
std::string after;
ASSERT_TRUE(base::ReadFileToString(path, &after));
EXPECT_EQ(before, after);
}
TEST_F(LaunchStateTest, LaunchStateGcAgeBoundaries) {
constexpr uint64_t kNow = kLaunchStateGcMaxAge + 100;
SetLaunchStateGcTimeForTesting(kNow);
EXPECT_EQ(LaunchStateGcAge::kInvalid, ClassifyLaunchStateGcAge(0));
EXPECT_EQ(LaunchStateGcAge::kFresh,
ClassifyLaunchStateGcAge(kNow - kLaunchStateGcMaxAge + 1));
EXPECT_EQ(LaunchStateGcAge::kExpired,
ClassifyLaunchStateGcAge(kNow - kLaunchStateGcMaxAge));
EXPECT_EQ(LaunchStateGcAge::kFresh, ClassifyLaunchStateGcAge(kNow + 1));
SetLaunchStateGcTimeForTesting(0);
EXPECT_EQ(LaunchStateGcAge::kUnavailable, ClassifyLaunchStateGcAge(kNow));
}
TEST_F(LaunchStateTest, OldSchemaAndFooterlessHealthAreRejectedAndPreserved) {
base::FilePath dir = GetLaunchStateDir(install_dir());
ASSERT_TRUE(base::CreateDirectory(dir));
const std::string old_schema =
R"({"appid":"test-app","pid":1,"pid_start_time":"1",)"
R"("consecutive_failures":0,"running":false,"confirmed":false,)"
R"("version":"150.1.0","platform":"windows64"})";
base::FilePath old_path = dir.Append(L"old-schema");
ASSERT_TRUE(WriteFileWithIntegrity(old_path, old_schema));
EXPECT_FALSE(ReadLaunchStatePath(old_path));
EXPECT_TRUE(base::PathExists(old_path));
const std::string current_schema =
R"({"appid":"test-app","pid":1,"pid_start_time":"1",)"
R"("consecutive_failures":0,"running":false,"confirmed":false,)"
R"("version":"150.1.0","platform":"windows64",)"
R"("last_update":"1"})";
base::FilePath footerless_path = dir.Append(L"footerless");
ASSERT_TRUE(base::WriteFile(footerless_path, current_schema));
EXPECT_FALSE(ReadLaunchStatePath(footerless_path));
EXPECT_TRUE(base::PathExists(footerless_path));
}
TEST_F(LaunchStateTest, InvalidHealthTimestampsAreRejected) {
base::FilePath dir = GetLaunchStateDir(install_dir());
ASSERT_TRUE(base::CreateDirectory(dir));
const char quote = 34;
const std::vector<std::string> values = {
std::string() + quote + "0" + quote, "-1",
std::string() + quote + "-1" + quote,
std::string() + quote + "18446744073709551616" + quote,
std::string() + quote + "not-a-time" + quote};
for (size_t i = 0; i < values.size(); ++i) {
const std::string json =
R"({"appid":"test-app","pid":1,"pid_start_time":"1",)"
R"("consecutive_failures":0,"running":false,"confirmed":false,)"
R"("version":"150.1.0","platform":"windows64","last_update":)" +
values[i] + "}";
base::FilePath path =
dir.AppendASCII("invalid-time-" + base::NumberToString(i));
ASSERT_TRUE(WriteFileWithIntegrity(path, json));
EXPECT_FALSE(ReadLaunchStatePath(path)) << values[i];
EXPECT_TRUE(base::PathExists(path));
}
}
TEST_F(LaunchStateTest, WriteLaunchStatePath_RejectsEmptyAppid) {
base::FilePath path =
install_dir().Append(L".launch").Append(L"abc123def4567890_1.0.0_win");
LaunchState state;
state.pid = 1;
state.pid_start_time = 1;
state.version = "1.0.0";
state.platform = "win";
EXPECT_FALSE(WriteLaunchStatePath(path, state));
}
TEST_F(LaunchStateTest, WriteLaunchStatePath_RejectsEmptyVersion) {
base::FilePath path =
install_dir().Append(L".launch").Append(L"abc123def4567890_1.0.0_win");
LaunchState state;
state.appid = "test";
state.pid = 1;
state.pid_start_time = 1;
state.platform = "win";
EXPECT_FALSE(WriteLaunchStatePath(path, state));
}
TEST_F(LaunchStateTest, WriteLaunchStatePath_RejectsEmptyPlatform) {
base::FilePath path =
install_dir().Append(L".launch").Append(L"abc123def4567890_1.0.0_win");
LaunchState state;
state.appid = "test";
state.pid = 1;
state.pid_start_time = 1;
state.version = "1.0.0";
EXPECT_FALSE(WriteLaunchStatePath(path, state));
}
TEST_F(LaunchStateTest, ReadLaunchStatePath_MissingFile) {
base::FilePath path =
install_dir().Append(L".launch").Append(L"abc123def4567890_1.0_win");
auto read = ReadLaunchStatePath(path);
EXPECT_FALSE(read.has_value());
}
TEST_F(LaunchStateTest, ReadLaunchStatePath_CorruptFile) {
base::FilePath launch_dir = install_dir().Append(L".launch");
ASSERT_TRUE(base::CreateDirectory(launch_dir));
base::FilePath path = launch_dir.Append(L"abc123def4567890_1.0_win");
ASSERT_TRUE(base::WriteFile(path, "corrupt garbage data!!"));
auto read = ReadLaunchStatePath(path);
EXPECT_FALSE(read.has_value());
}
TEST_F(LaunchStateTest, ScanLaunchStates_MultipleVersions) {
base::FilePath install = install_dir();
std::wstring hash = L"abc123def4567890";
for (const char* ver : {"150.1.0", "150.2.0", "150.3.0"}) {
LaunchState ls;
ls.appid = "test-app";
ls.pid = 1;
ls.pid_start_time = 1;
ls.consecutive_failures = 0;
ls.running = false;
ls.version = ver;
ls.platform = GetCurrentPlatform();
base::FilePath path = GetInstallDirLaunchStatePath(
install, hash, Version::Parse(ver), GetCurrentPlatform());
ASSERT_TRUE(WriteLaunchStatePath(path, ls));
}
auto states = ScanLaunchStates(install, hash);
ASSERT_EQ(3u, states.size());
EXPECT_TRUE(states.count(Version::Parse("150.1.0")));
EXPECT_TRUE(states.count(Version::Parse("150.2.0")));
EXPECT_TRUE(states.count(Version::Parse("150.3.0")));
EXPECT_EQ("test-app", states.at(Version::Parse("150.1.0")).appid);
EXPECT_EQ("150.2.0", states.at(Version::Parse("150.2.0")).version);
}
TEST_F(LaunchStateTest, ScanLaunchStates_FiltersAppidHash) {
base::FilePath install = install_dir();
std::wstring hash1 = L"aaaa000000000000";
std::wstring hash2 = L"bbbb000000000000";
std::string platform = GetCurrentPlatform();
LaunchState ls;
ls.appid = "app1";
ls.pid = 1;
ls.pid_start_time = 1;
ls.version = "150.1.0";
ls.platform = platform;
ASSERT_TRUE(WriteLaunchStatePath(
GetInstallDirLaunchStatePath(install, hash1, Version::Parse("150.1.0"),
platform),
ls));
ls.appid = "app2";
ls.version = "150.2.0";
ASSERT_TRUE(WriteLaunchStatePath(
GetInstallDirLaunchStatePath(install, hash2, Version::Parse("150.2.0"),
platform),
ls));
auto states = ScanLaunchStates(install, hash1);
ASSERT_EQ(1u, states.size());
EXPECT_TRUE(states.count(Version::Parse("150.1.0")));
EXPECT_EQ("app1", states.at(Version::Parse("150.1.0")).appid);
}
TEST_F(LaunchStateTest, ScanLaunchStates_SkipsMalformed) {
base::FilePath install = install_dir();
std::wstring hash = L"abc123def4567890";
std::string platform = GetCurrentPlatform();
LaunchState ls;
ls.appid = "test-app";
ls.pid = 1;
ls.pid_start_time = 1;
ls.version = "150.1.0";
ls.platform = platform;
ASSERT_TRUE(WriteLaunchStatePath(
GetInstallDirLaunchStatePath(install, hash, Version::Parse("150.1.0"),
platform),
ls));
// Write a garbage file with the right prefix.
base::FilePath launch_dir = GetLaunchStateDir(install);
base::FilePath garbage = launch_dir.Append(hash + L"_garbage");
ASSERT_TRUE(base::WriteFile(garbage, "not a valid file"));
auto states = ScanLaunchStates(install, hash);
ASSERT_EQ(1u, states.size());
EXPECT_TRUE(states.count(Version::Parse("150.1.0")));
}
TEST_F(LaunchStateTest, ScanLaunchStates_EmptyDir) {
base::FilePath install = install_dir();
ASSERT_TRUE(base::CreateDirectory(GetLaunchStateDir(install)));
auto states = ScanLaunchStates(install, L"abc123def4567890");
EXPECT_TRUE(states.empty());
}
TEST_F(LaunchStateTest, ScanLaunchStates_NoDir) {
auto states = ScanLaunchStates(install_dir(), L"abc123def4567890");
EXPECT_TRUE(states.empty());
}
TEST_F(LaunchStateTest, LivenessFilenameCannotCollideWithHealthSentinel) {
std::wstring hash = L"abc123def4567890";
std::string platform = GetCurrentPlatform();
base::FilePath liveness =
GetInstallDirLivenessPath(install_dir(), hash, platform);
base::FilePath health = GetInstallDirLaunchStatePath(
install_dir(), hash, Version::Parse("150.1.0"), platform);
EXPECT_NE(liveness, health);
EXPECT_EQ(hash + L"_" + base::UTF8ToWide(platform),
liveness.BaseName().value());
}
TEST_F(LaunchStateTest, LivenessRoundTrip) {
base::FilePath path = GetInstallDirLivenessPath(
install_dir(), L"abc123def4567890", GetCurrentPlatform());
LivenessRecord record{"test-app", GetCurrentPlatform(), 123456};
ASSERT_TRUE(WriteLivenessPath(path, record));
std::optional<LivenessRecord> read = ReadLivenessPath(path);
ASSERT_TRUE(read);
EXPECT_EQ(record.appid, read->appid);
EXPECT_EQ(record.platform, read->platform);
EXPECT_EQ(record.last_launch, read->last_launch);
EXPECT_FALSE(ReadLaunchStatePath(path));
}
TEST_F(LaunchStateTest, LivenessRejectsZeroAndFooterlessRecords) {
base::FilePath path = GetInstallDirLivenessPath(
install_dir(), L"abc123def4567890", GetCurrentPlatform());
EXPECT_FALSE(WriteLivenessPath(path, {"test-app", GetCurrentPlatform(), 0}));
EXPECT_FALSE(RefreshLivenessPath(path, "test-app", GetCurrentPlatform(), 0));
ASSERT_TRUE(base::CreateDirectory(path.DirName()));
ASSERT_TRUE(base::WriteFile(
path,
R"({"appid":"test-app","platform":"windows64","last_launch":"1"})"));
EXPECT_FALSE(ReadLivenessPath(path));
EXPECT_TRUE(base::PathExists(path));
}
TEST_F(LaunchStateTest, InvalidLivenessTimestampsAreRejectedAndPreserved) {
base::FilePath path = GetInstallDirLivenessPath(
install_dir(), L"abc123def4567890", GetCurrentPlatform());
ASSERT_TRUE(base::CreateDirectory(path.DirName()));
const std::vector<std::string> invalid_records = {
R"({"appid":"test-app","platform":"windows64"})",
R"({"appid":"test-app","platform":"windows64","last_launch":1})",
R"({"appid":"test-app","platform":"windows64","last_launch":"-1"})",
R"({"appid":"test-app","platform":"windows64","last_launch":"18446744073709551616"})",
R"({"appid":"test-app","platform":"windows64","last_launch":"invalid"})",
R"({"appid":"test-app","platform":"windows64","last_launch":"0"})",
};
for (const auto& json : invalid_records) {
ASSERT_TRUE(WriteFileWithIntegrity(path, json));
EXPECT_FALSE(ReadLivenessPath(path)) << json;
EXPECT_TRUE(base::PathExists(path)) << json;
}
}
TEST_F(LaunchStateTest, LaunchRecordSnapshotBindsExactPublication) {
const std::string appid = "test-app";
const std::string platform = GetCurrentPlatform();
base::FilePath path =
GetInstallDirLivenessPath(install_dir(), GetAppidHash(appid), platform);
ASSERT_TRUE(WriteLivenessPath(path, {appid, platform, 100}));
std::optional<base::FilePath> resolved =
GetSafeDirectoryResolvedPath(path.DirName());
ASSERT_TRUE(resolved);
std::optional<LaunchRecordSnapshot> snapshot =
ReadLaunchRecordSnapshot(path, *resolved);
ASSERT_TRUE(snapshot);
EXPECT_EQ(LaunchRecordKind::kLiveness, snapshot->kind);
EXPECT_EQ(100u, snapshot->liveness.last_launch);
ASSERT_TRUE(WriteLivenessPath(path, {appid, platform, 200}));
EXPECT_EQ(
ConditionalDeleteResult::kChanged,
DeleteFileWithIntegrityIfMatching(snapshot->path, snapshot->content,
/*expected_integrity_protected=*/true,
snapshot->resolved_parent_path));
EXPECT_EQ(200u, ReadLivenessPath(path)->last_launch);
}
TEST_F(LaunchStateTest, LaunchRecordSnapshotClassifiesInvalidPublications) {
const std::string platform = GetCurrentPlatform();
base::FilePath path =
GetInstallDirLivenessPath(install_dir(), L"abc123def4567890", platform);
ASSERT_TRUE(base::CreateDirectory(path.DirName()));
std::optional<base::FilePath> resolved =
GetSafeDirectoryResolvedPath(path.DirName());
ASSERT_TRUE(resolved);
ASSERT_TRUE(WriteFileWithIntegrity(path, "{}"));
std::optional<LaunchRecordSnapshot> malformed =
ReadLaunchRecordSnapshot(path, *resolved);
ASSERT_TRUE(malformed);
EXPECT_EQ(LaunchRecordKind::kInvalid, malformed->kind);
EXPECT_FALSE(malformed->raw_content.empty());
EXPECT_FALSE(malformed->oversized);
const std::string footerless =
R"({"appid":"test-app","platform":"windows64","last_launch":"1"})";
ASSERT_TRUE(base::WriteFile(path, footerless));
std::optional<LaunchRecordSnapshot> no_footer =
ReadLaunchRecordSnapshot(path, *resolved);
ASSERT_TRUE(no_footer);
EXPECT_EQ(LaunchRecordKind::kInvalid, no_footer->kind);
EXPECT_EQ(footerless, no_footer->raw_content);
ASSERT_TRUE(WriteLivenessPath(path, {"test-app", platform, 1}));
std::string corrupt;
ASSERT_TRUE(base::ReadFileToString(path, &corrupt));
ASSERT_FALSE(corrupt.empty());
corrupt[0] ^= 1;
ASSERT_TRUE(base::WriteFile(path, corrupt));
std::optional<LaunchRecordSnapshot> crc_mismatch =
ReadLaunchRecordSnapshot(path, *resolved);
ASSERT_TRUE(crc_mismatch);
EXPECT_EQ(LaunchRecordKind::kInvalid, crc_mismatch->kind);
EXPECT_EQ(corrupt, crc_mismatch->raw_content);
ASSERT_TRUE(
base::WriteFile(path, std::string(kMaxLaunchStateFileSize + 17, 'x')));
std::optional<LaunchRecordSnapshot> oversized =
ReadLaunchRecordSnapshot(path, *resolved);
ASSERT_TRUE(oversized);
EXPECT_EQ(LaunchRecordKind::kInvalid, oversized->kind);
EXPECT_TRUE(oversized->oversized);
std::optional<base::FilePath> wrong_parent =
GetSafeDirectoryResolvedPath(install_dir());
ASSERT_TRUE(wrong_parent);
EXPECT_FALSE(ReadLaunchRecordSnapshot(path, *wrong_parent));
ASSERT_TRUE(base::DeleteFile(path));
base::FilePath target = install_dir().Append(L"reparse-target");
ASSERT_TRUE(WriteFileWithIntegrity(target, "target"));
constexpr DWORD kAllowUnprivilegedCreate = 0x2;
if (!::CreateSymbolicLinkW(path.value().c_str(), target.value().c_str(),
kAllowUnprivilegedCreate)) {
GTEST_SKIP() << "Symbolic-link creation is unavailable";
}
std::optional<LaunchRecordSnapshot> reparse =
ReadLaunchRecordSnapshot(path, *resolved);
ASSERT_TRUE(reparse);
EXPECT_EQ(LaunchRecordKind::kInvalid, reparse->kind);
EXPECT_TRUE(reparse->reparse_point);
}
TEST_F(LaunchStateTest, LivenessRefreshUsesContentTimestamp) {
base::FilePath path = GetInstallDirLivenessPath(
install_dir(), L"abc123def4567890", GetCurrentPlatform());
constexpr uint64_t now = 100 * kLivenessRefreshIntervalFileTimeTicks;
ASSERT_TRUE(WriteLivenessPath(
path, {"test-app", GetCurrentPlatform(),
now - kLivenessRefreshIntervalFileTimeTicks + 1}));
ASSERT_TRUE(RefreshLivenessPath(path, "test-app", GetCurrentPlatform(), now));
ASSERT_EQ(now - kLivenessRefreshIntervalFileTimeTicks + 1,
ReadLivenessPath(path)->last_launch);
ASSERT_TRUE(
WriteLivenessPath(path, {"test-app", GetCurrentPlatform(),
now - kLivenessRefreshIntervalFileTimeTicks}));
ASSERT_TRUE(RefreshLivenessPath(path, "test-app", GetCurrentPlatform(), now));
EXPECT_EQ(now, ReadLivenessPath(path)->last_launch);
}
TEST_F(LaunchStateTest, LivenessFutureTimestampIsReplacedAfterClockRollback) {
base::FilePath path = GetInstallDirLivenessPath(
install_dir(), L"abc123def4567890", GetCurrentPlatform());
ASSERT_TRUE(WriteLivenessPath(path, {"test-app", GetCurrentPlatform(), 200}));
ASSERT_TRUE(RefreshLivenessPath(path, "test-app", GetCurrentPlatform(), 100));
EXPECT_EQ(100u, ReadLivenessPath(path)->last_launch);
}
TEST_F(LaunchStateTest, LivenessMalformedOrOversizedIsReplaced) {
base::FilePath path = GetInstallDirLivenessPath(
install_dir(), L"abc123def4567890", GetCurrentPlatform());
ASSERT_TRUE(base::CreateDirectory(path.DirName()));
ASSERT_TRUE(
base::WriteFile(path, std::string(kMaxLaunchStateFileSize + 1, 'x')));
ASSERT_TRUE(RefreshLivenessPath(path, "test-app", GetCurrentPlatform(), 123));
ASSERT_EQ(123u, ReadLivenessPath(path)->last_launch);
}
TEST_F(LaunchStateTest, RetentionCollectorChoosesNewestEvidence) {
const std::string appid = "retention-app";
const std::string platform = GetCurrentPlatform();
const std::wstring hash = GetAppidHash(appid);
ASSERT_TRUE(WriteLivenessPath(
GetInstallDirLivenessPath(install_dir(), hash, platform),
{appid, platform, 100}));
LaunchState older_health;
older_health.appid = appid;
older_health.pid_start_time = 90;
older_health.version = "150.1.0";
older_health.platform = platform;
ASSERT_TRUE(WriteLaunchStatePath(
GetInstallDirLaunchStatePath(
install_dir(), hash, Version::Parse(older_health.version), platform),
older_health));
LaunchState newest_health = older_health;
newest_health.pid_start_time = 120;
newest_health.version = "151.1.0";
ASSERT_TRUE(WriteLaunchStatePath(
GetInstallDirLaunchStatePath(
install_dir(), hash, Version::Parse(newest_health.version), platform),
newest_health));
RetentionEvidenceMap evidence =
CollectRetentionEvidence(install_dir(), {{"retention-app", platform}});
const RetentionEvidence& result = evidence.at({appid, platform});
EXPECT_FALSE(result.unknown);
EXPECT_EQ(RetentionEvidenceKind::kHealthSentinel, result.kind);
EXPECT_EQ(120u, result.timestamp);
}
TEST_F(LaunchStateTest, RetentionCollectorIsPlatformScoped) {
const std::string appid = "retention-app";
const std::wstring hash = GetAppidHash(appid);
ASSERT_TRUE(WriteLivenessPath(
GetInstallDirLivenessPath(install_dir(), hash, "windows64"),
{appid, "windows64", 100}));
ASSERT_TRUE(WriteLivenessPath(
GetInstallDirLivenessPath(install_dir(), hash, "windows32"),
{appid, "windows32", 200}));
RetentionEvidenceMap evidence = CollectRetentionEvidence(
install_dir(), {{appid, "windows64"}, {appid, "windows32"}});
EXPECT_EQ(100u, evidence.at({appid, "windows64"}).timestamp);
EXPECT_EQ(200u, evidence.at({appid, "windows32"}).timestamp);
}
TEST_F(LaunchStateTest,
RetentionSnapshotUsesOneCanonicalObservationForPlanAndCleanup) {
const std::string appid = "retention-app";
const std::string platform = GetCurrentPlatform();
base::FilePath path =
GetInstallDirLivenessPath(install_dir(), GetAppidHash(appid), platform);
ASSERT_TRUE(WriteLivenessPath(path, {appid, platform, 100}));
SetRetentionCanonicalObservationHookForTesting(base::BindRepeating(
[](const std::string& hook_appid, const std::string& hook_platform,
const base::FilePath& hook_path) {
ASSERT_TRUE(
WriteLivenessPath(hook_path, {hook_appid, hook_platform, 200}));
},
appid, platform));
RetentionEvidenceSnapshot snapshot =
CollectRetentionEvidenceSnapshot(install_dir(), {{appid, platform}});
ASSERT_EQ(1u, snapshot.files.size());
EXPECT_EQ(100u, snapshot.evidence.at({appid, platform}).timestamp);
EXPECT_EQ(ConditionalDeleteResult::kChanged,
DeleteFileWithIntegrityIfMatching(
path, snapshot.files[0].content,
snapshot.files[0].integrity_protected,
snapshot.files[0].resolved_parent_path));
EXPECT_EQ(200u, ReadLivenessPath(path)->last_launch);
}
TEST_F(LaunchStateTest, RetentionCollectorProtectsNoncanonicalEvidence) {
const std::string appid = "retention-app";
const std::string platform = GetCurrentPlatform();
base::FilePath path =
GetLaunchStateDir(install_dir()).Append(L"noncanonical");
ASSERT_TRUE(WriteLivenessPath(path, {appid, platform, 100}));
RetentionEvidenceMap evidence =
CollectRetentionEvidence(install_dir(), {{appid, platform}});
EXPECT_TRUE(evidence.at({appid, platform}).unknown);
EXPECT_EQ("invalid_or_noncanonical_evidence",
evidence.at({appid, platform}).diagnostic);
}
TEST_F(LaunchStateTest, RetentionCollectorProtectsMalformedCanonicalEvidence) {
const std::string appid = "retention-app";
const std::string platform = GetCurrentPlatform();
base::FilePath path =
GetInstallDirLivenessPath(install_dir(), GetAppidHash(appid), platform);
ASSERT_TRUE(base::CreateDirectory(path.DirName()));
ASSERT_TRUE(base::WriteFile(path, "malformed"));
RetentionEvidenceMap evidence =
CollectRetentionEvidence(install_dir(), {{appid, platform}});
EXPECT_TRUE(evidence.at({appid, platform}).unknown);
EXPECT_EQ("invalid_or_noncanonical_evidence",
evidence.at({appid, platform}).diagnostic);
EXPECT_TRUE(base::PathExists(path));
}
TEST_F(LaunchStateTest, RetentionCollectorPreservesIntegrityMismatch) {
const std::string appid = "retention-app";
const std::string platform = GetCurrentPlatform();
base::FilePath path =
GetInstallDirLivenessPath(install_dir(), GetAppidHash(appid), platform);
ASSERT_TRUE(WriteLivenessPath(path, {appid, platform, 100}));
std::string corrupt;
ASSERT_TRUE(base::ReadFileToString(path, &corrupt));
ASSERT_GT(corrupt.size(), 20u);
corrupt[5] ^= 0xFF;
ASSERT_TRUE(base::WriteFile(path, corrupt));
RetentionEvidenceMap evidence =
CollectRetentionEvidence(install_dir(), {{appid, platform}});
EXPECT_TRUE(evidence.at({appid, platform}).unknown);
std::string after;
ASSERT_TRUE(base::ReadFileToString(path, &after));
EXPECT_EQ(corrupt, after);
}
TEST_F(LaunchStateTest, RetentionCollectorMissingDirectoryIsProtected) {
RetentionEvidenceMap evidence = CollectRetentionEvidence(
install_dir(), {{"retention-app", GetCurrentPlatform()}});
ASSERT_EQ(1u, evidence.size());
const RetentionEvidence& result =
evidence.at({"retention-app", GetCurrentPlatform()});
EXPECT_EQ(RetentionEvidenceKind::kNone, result.kind);
EXPECT_EQ(0u, result.timestamp);
EXPECT_FALSE(result.unknown);
}
TEST_F(LaunchStateTest, RetentionCollectorDoesNotWrite) {
const std::string appid = "retention-app";
const std::string platform = GetCurrentPlatform();
base::FilePath path =
GetInstallDirLivenessPath(install_dir(), GetAppidHash(appid), platform);
ASSERT_TRUE(WriteLivenessPath(path, {appid, platform, 100}));
std::string before;
ASSERT_TRUE(base::ReadFileToString(path, &before));
CollectRetentionEvidence(install_dir(), {{appid, platform}});
std::string after;
ASSERT_TRUE(base::ReadFileToString(path, &after));
EXPECT_EQ(before, after);
}
} // namespace cef_installer
@@ -0,0 +1,348 @@
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cef/libcef_dll/bootstrap/installer/installer_lifecycle.h"
#include <algorithm>
#include <array>
#include <limits>
#include <type_traits>
#include "base/containers/span.h"
#include "base/json/json_writer.h"
#include "base/rand_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "cef/libcef_dll/bootstrap/installer/installer_constants.h"
#include "cef/libcef_dll/bootstrap/installer/installer_logger.h"
#include "cef/libcef_dll/bootstrap/installer/installer_result_json.h"
namespace cef_installer {
namespace {
constexpr char kRelaunchStartedEvent[] = "relaunch_started";
constexpr char kOperationResultEvent[] = "operation_result";
constexpr char kDiagnosticFallback[] = "Diagnostic unavailable";
static_assert(std::numeric_limits<DWORD>::max() <= UINT64_C(9007199254740991));
static_assert(std::numeric_limits<int>::digits <= 53);
static_assert(std::is_same_v<decltype(COPYDATASTRUCT::dwData), ULONG_PTR>);
static_assert(sizeof(kWmCopyDataInstallerLifecycle) <=
sizeof(COPYDATASTRUCT::dwData));
bool SerializeDictionary(const base::DictValue& dict, std::string* json) {
return base::JSONWriter::Write(dict, json) &&
json->size() + 1 <= kInstallerLifecycleMaxPayloadSize;
}
bool FitsWithTruncationFlag(const base::DictValue& dict) {
base::DictValue candidate = dict.Clone();
candidate.Set("diagnostics_truncated", true);
std::string json;
return SerializeDictionary(candidate, &json);
}
std::string BoundDiagnostic(std::string_view input,
size_t max_size,
bool* truncated) {
if (!base::IsStringUTF8(input)) {
*truncated = true;
return kDiagnosticFallback;
}
std::string_view bounded = base::TruncateUTF8ToByteSize(input, max_size);
if (bounded.size() != input.size()) {
*truncated = true;
}
return std::string(bounded);
}
std::optional<std::string> SerializeLifecycleDictionary(base::DictValue dict,
const Result* result) {
bool diagnostics_truncated = false;
std::string error_message;
std::vector<std::string> warnings;
if (result && !result->success) {
error_message = BoundDiagnostic(result->error_message,
kInstallerLifecycleMaxErrorMessageSize,
&diagnostics_truncated);
}
if (result) {
const size_t count =
std::min(result->warnings.size(),
static_cast<size_t>(kInstallerLifecycleMaxWarnings));
diagnostics_truncated |= count != result->warnings.size();
warnings.reserve(count);
for (size_t i = 0; i < count; ++i) {
warnings.push_back(BoundDiagnostic(result->warnings[i],
kInstallerLifecycleMaxWarningSize,
&diagnostics_truncated));
}
}
dict.Remove("error_message");
dict.Remove("warnings");
if (result && !result->success) {
dict.Set("error_message", error_message);
if (!FitsWithTruncationFlag(dict)) {
diagnostics_truncated = true;
size_t low = 0;
size_t high = error_message.size();
while (low < high) {
const size_t middle = low + (high - low + 1) / 2;
base::DictValue candidate = dict.Clone();
candidate.Set("error_message", std::string(base::TruncateUTF8ToByteSize(
error_message, middle)));
if (FitsWithTruncationFlag(candidate)) {
low = middle;
} else {
high = middle - 1;
}
}
dict.Set("error_message",
std::string(base::TruncateUTF8ToByteSize(error_message, low)));
if (!FitsWithTruncationFlag(dict)) {
return std::nullopt;
}
}
} else if (!FitsWithTruncationFlag(dict)) {
return std::nullopt;
}
base::ListValue included_warnings;
for (const auto& warning : warnings) {
base::ListValue candidate_warnings = included_warnings.Clone();
candidate_warnings.Append(warning);
base::DictValue candidate = dict.Clone();
candidate.Set("warnings", std::move(candidate_warnings));
if (FitsWithTruncationFlag(candidate)) {
included_warnings.Append(warning);
continue;
}
diagnostics_truncated = true;
size_t low = 0;
size_t high = warning.size();
while (low < high) {
const size_t middle = low + (high - low + 1) / 2;
base::ListValue shortened = included_warnings.Clone();
shortened.Append(
std::string(base::TruncateUTF8ToByteSize(warning, middle)));
base::DictValue shortened_candidate = dict.Clone();
shortened_candidate.Set("warnings", std::move(shortened));
if (FitsWithTruncationFlag(shortened_candidate)) {
low = middle;
} else {
high = middle - 1;
}
}
const std::string shortened_warning(
base::TruncateUTF8ToByteSize(warning, low));
if (!shortened_warning.empty()) {
included_warnings.Append(shortened_warning);
}
break;
}
if (included_warnings.size() != warnings.size()) {
diagnostics_truncated = true;
}
if (!included_warnings.empty()) {
dict.Set("warnings", std::move(included_warnings));
}
if (diagnostics_truncated) {
dict.Set("diagnostics_truncated", true);
}
std::string json;
if (!SerializeDictionary(dict, &json)) {
return std::nullopt;
}
return json;
}
} // namespace
bool UninstallLifecycleContext::valid() const {
return parent_window && IsValidInstallerLifecycleOperationId(operation_id);
}
std::string GenerateInstallerLifecycleOperationId() {
std::array<uint8_t, 16> random = {};
base::RandBytes(random);
return base::ToLowerASCII(base::HexEncode(random));
}
bool IsValidInstallerLifecycleOperationId(std::string_view operation_id) {
if (operation_id.size() != 32) {
return false;
}
return std::all_of(operation_id.begin(), operation_id.end(), [](char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
});
}
bool IsValidInstallerLifecycleTerminalErrorCode(int exit_code) {
switch (exit_code) {
case kExitCodeConfigError:
case kExitCodeNetworkError:
case kExitCodeSignatureError:
case kExitCodeNoMatchingVersion:
case kExitCodeExtractionError:
case kExitCodeInstallError:
case kExitCodeDatabaseError:
case kExitCodeLockTimeout:
case kExitCodeCancelled:
case kExitCodeNoSentinel:
case kExitCodeSentinelReadError:
case kExitCodeSentinelOwnerMismatch:
case kExitCodePolicyDenied:
case kExitCodeIndexError:
case kExitCodeRecoveryError:
case kExitCodeRepairError:
case kExitCodeQuarantineError:
case kExitCodeRetentionSnapshotChanged:
case kExitCodePolicyError:
case kExitCodeUnknownError:
return true;
case kExitCodeSuccess:
case kExitCodeRelaunched:
return false;
}
return false;
}
std::optional<std::string> SerializeInstallerRelaunchStarted(
std::string_view operation_id,
DWORD child_pid) {
if (!IsValidInstallerLifecycleOperationId(operation_id) || child_pid == 0) {
return std::nullopt;
}
base::DictValue dict;
dict.Set("protocol_version", kInstallerLifecycleProtocolVersion);
dict.Set("event", kRelaunchStartedEvent);
dict.Set("operation_id", operation_id);
dict.Set("child_pid", static_cast<double>(child_pid));
return SerializeLifecycleDictionary(std::move(dict), nullptr);
}
std::optional<std::string> SerializeInstallerOperationResult(
std::string_view operation_id,
const Result& result,
int exit_code) {
if (!IsValidInstallerLifecycleOperationId(operation_id) ||
exit_code != ResultToExitCode(result) ||
(!result.success &&
!IsValidInstallerLifecycleTerminalErrorCode(exit_code))) {
return std::nullopt;
}
// base::Value requires valid UTF-8 at construction. Substitute invalid
// diagnostics before building the shared dictionary; the bounded helper
// below uses the original Result so it can also set diagnostics_truncated.
Result serializable_result = result;
if (!serializable_result.success &&
!base::IsStringUTF8(serializable_result.error_message)) {
serializable_result.error_message = kDiagnosticFallback;
}
for (std::string& warning : serializable_result.warnings) {
if (!base::IsStringUTF8(warning)) {
warning = kDiagnosticFallback;
}
}
base::DictValue dict = internal::BuildResultJsonValue(serializable_result);
dict.Set("protocol_version", kInstallerLifecycleProtocolVersion);
dict.Set("event", kOperationResultEvent);
dict.Set("operation_id", operation_id);
dict.Set("command", "uninstall");
dict.Set("exit_code", exit_code);
return SerializeLifecycleDictionary(std::move(dict), &result);
}
InstallerLifecycleSendStatus SendInstallerLifecycleMessage(
HWND parent_window,
std::string_view json) {
if (!parent_window || !::IsWindow(parent_window)) {
return InstallerLifecycleSendStatus::kInvalidWindow;
}
if (json.empty() || !base::IsStringUTF8(json) ||
json.find('\0') != std::string_view::npos ||
json.size() + 1 > kInstallerLifecycleMaxPayloadSize) {
return InstallerLifecycleSendStatus::kSerializationError;
}
const std::string payload(json);
COPYDATASTRUCT data = {};
data.dwData = kWmCopyDataInstallerLifecycle;
data.cbData = static_cast<DWORD>(json.size() + 1);
data.lpData = const_cast<char*>(payload.c_str());
DWORD_PTR receiver_result = 0;
::SetLastError(ERROR_SUCCESS);
const LRESULT sent = ::SendMessageTimeoutW(
parent_window, WM_COPYDATA, 0, reinterpret_cast<LPARAM>(&data),
SMTO_ABORTIFHUNG, kInstallerLifecycleSendTimeoutMs, &receiver_result);
if (sent != 0) {
return InstallerLifecycleSendStatus::kDelivered;
}
return ::GetLastError() == ERROR_TIMEOUT
? InstallerLifecycleSendStatus::kTimeout
: InstallerLifecycleSendStatus::kError;
}
int FinalizeUninstallLifecycle(
const std::optional<UninstallLifecycleContext>& context,
const Result& result) {
const int exit_code = ResultToExitCode(result);
if (!context || !context->valid()) {
return exit_code;
}
std::optional<std::string> json = SerializeInstallerOperationResult(
context->operation_id, result, exit_code);
const InstallerLifecycleSendStatus status =
json ? SendInstallerLifecycleMessage(context->parent_window, *json)
: InstallerLifecycleSendStatus::kSerializationError;
if (status != InstallerLifecycleSendStatus::kDelivered) {
Logger::GetInstance().Warning(
"Uninstall lifecycle terminal delivery was not confirmed");
}
return exit_code;
}
int FinalizeControlledUninstallFailure(
const std::optional<UninstallLifecycleContext>& context,
ControlledUninstallFailure failure,
int error_code) {
const char* diagnostic = nullptr;
switch (failure) {
case ControlledUninstallFailure::kInvalidRetentionOptions:
diagnostic = "Invalid retention option usage";
break;
case ControlledUninstallFailure::kInvalidRetentionAge:
diagnostic = "Invalid retention age";
break;
case ControlledUninstallFailure::kConfigLoad:
diagnostic = "Failed to load installer configuration";
break;
case ControlledUninstallFailure::kConfigNotFound:
diagnostic = "Installer configuration was not found";
break;
case ControlledUninstallFailure::kExplicitModeDisabled:
diagnostic = "Explicit installer command is not enabled";
break;
case ControlledUninstallFailure::kPreflightRejected:
diagnostic = "Relaunched uninstall preflight was rejected";
break;
}
return FinalizeUninstallLifecycle(context,
Result::Error(error_code, diagnostic));
}
int FinalizeUninstallLifecycleAfterProgressUi(
const std::optional<UninstallLifecycleContext>& context,
const Result& result,
base::OnceClosure close_progress_ui) {
if (close_progress_ui) {
std::move(close_progress_ui).Run();
}
return FinalizeUninstallLifecycle(context, result);
}
} // namespace cef_installer
Loaded 100 of 202 files, more files were not shown because too many files have changed in this diff. Show more