bootstrap: Add sandbox compatibility hash (fixes #4092)

Introduce a hash of Chromium sandbox interface files to detect
incompatibilities between bootstrap.exe and libcef.dll at runtime,
allowing the strict version check to be skipped when interfaces match.

Core implementation:
- Add sandbox_compat_hash.py to calculate SHA-256 of interface files
  (chrome_elf_main.h, sandbox.h, sandbox_types.h, sandbox.mojom)
- Update make_version_header.py to generate CEF_SANDBOX_COMPAT_HASH
- Add sandbox_compat_hash field to cef_version_info_t struct
- Add entry 3 to cef_api_hash() for retrieving the hash
- Compare hashes in cef_scoped_library_loader_win.cc during load

Version management:
- Include sandbox_compat in cef_api_versions.json and cef_api_untracked.json
- Compare sandbox_compat for untracked versions (NEXT, EXPERIMENTAL)
- Trigger version bumps when sandbox interface files change

Build automation:
- Update cefbuilds JSON builder to include sandbox_compat in version records
- Run version_manager.py -a after Chromium updates on release branches
  to auto-detect hash changes and add new API versions
This commit is contained in:
Marshall Greenblatt committed 2026-02-16 17:19:33 -05:00
1 parent 58fa5d380a
commit 5d801ed841
11 files changed
+656 -35

No files matched your search

+14 -1
View File
@@ -1584,8 +1584,21 @@ action("make_version_header") {
"//chrome/VERSION",
]
outputs = [ "$root_gen_dir/cef/include/cef_version.h" ]
args = rebase_path(outputs, root_build_dir)
if (is_win) {
# Sandbox interface files for Windows-only compatibility hash.
_sandbox_compat_files = [
"//chrome/chrome_elf/chrome_elf_main.h",
"//sandbox/policy/mojom/sandbox.mojom",
"//sandbox/win/src/sandbox.h",
"//sandbox/win/src/sandbox_types.h",
]
inputs += _sandbox_compat_files
args += [ "--sandbox-files" ]
args += rebase_path(_sandbox_compat_files, root_build_dir)
}
}
# This no-op action lists args.gn as an output, allowing it to be referenced as
+1
View File
@@ -131,6 +131,7 @@ extern "C" {
/// 0 - CEF_API_HASH_PLATFORM
/// 1 - CEF_API_HASH_UNIVERSAL (deprecated, same as CEF_API_HASH_PLATFORM)
/// 2 - CEF_COMMIT_HASH (from cef_version.h)
/// 3 - CEF_SANDBOX_COMPAT_HASH (from cef_version.h, Windows only)
///
CEF_EXPORT const char* cef_api_hash(int version, int entry);
+30 -1
View File
@@ -31,6 +31,7 @@
#define CEF_INCLUDE_CEF_VERSION_INFO_H_
#include <stddef.h>
#include <string.h>
#include "include/cef_api_hash.h"
#include "include/internal/cef_export.h"
@@ -77,8 +78,25 @@ typedef struct _cef_version_info_t {
int chrome_version_minor;
int chrome_version_build;
int chrome_version_patch;
#if CEF_API_ADDED(CEF_NEXT)
///
/// Sandbox compatibility hash (Windows only, empty on other platforms).
///
char sandbox_compat_hash[17];
#endif
} cef_version_info_t;
#if CEF_API_ADDED(CEF_NEXT)
///
/// Minimum struct size required to contain the sandbox_compat_hash field.
/// Used for version compatibility checks.
///
#define CEF_VERSION_INFO_SIZE_WITH_SANDBOX_HASH \
(offsetof(cef_version_info_t, sandbox_compat_hash) + \
sizeof(((cef_version_info_t*)0)->sandbox_compat_hash))
#endif
///
/// Return all CEF version information for the libcef library.
///
@@ -104,6 +122,16 @@ typedef struct _cef_version_info_t {
#endif // !defined(GENERATING_CEF_API_HASH)
// Helper macro for setting sandbox_compat_hash.
#if CEF_API_ADDED(CEF_NEXT)
#define _CEF_POPULATE_SANDBOX_HASH(info) \
strncpy((info)->sandbox_compat_hash, CEF_SANDBOX_COMPAT_HASH, \
sizeof((info)->sandbox_compat_hash) - 1); \
(info)->sandbox_compat_hash[sizeof((info)->sandbox_compat_hash) - 1] = '\0'
#else
#define _CEF_POPULATE_SANDBOX_HASH(info)
#endif
///
/// Populate CEF version information for the client library.
///
@@ -116,7 +144,8 @@ typedef struct _cef_version_info_t {
(info)->chrome_version_major = CHROME_VERSION_MAJOR; \
(info)->chrome_version_minor = CHROME_VERSION_MINOR; \
(info)->chrome_version_build = CHROME_VERSION_BUILD; \
(info)->chrome_version_patch = CHROME_VERSION_PATCH
(info)->chrome_version_patch = CHROME_VERSION_PATCH; \
_CEF_POPULATE_SANDBOX_HASH(info)
#ifdef __cplusplus
}
+15
View File
@@ -46,6 +46,11 @@ CEF_EXPORT int cef_version_info(int entry) {
}
CEF_EXPORT void cef_version_info_all(cef_version_info_t* info) {
if (!info || info->size == 0) {
return;
}
// Always populate base version fields (present in all struct versions).
info->cef_version_major = CEF_VERSION_MAJOR;
info->cef_version_minor = CEF_VERSION_MINOR;
info->cef_version_patch = CEF_VERSION_PATCH;
@@ -54,6 +59,14 @@ CEF_EXPORT void cef_version_info_all(cef_version_info_t* info) {
info->chrome_version_minor = CHROME_VERSION_MINOR;
info->chrome_version_build = CHROME_VERSION_BUILD;
info->chrome_version_patch = CHROME_VERSION_PATCH;
// Populate sandbox_compat_hash if the struct is large enough to contain it.
// This allows older clients with smaller structs to still work.
if (info->size >= CEF_VERSION_INFO_SIZE_WITH_SANDBOX_HASH) {
strncpy(info->sandbox_compat_hash, CEF_SANDBOX_COMPAT_HASH,
sizeof(info->sandbox_compat_hash) - 1);
info->sandbox_compat_hash[sizeof(info->sandbox_compat_hash) - 1] = '\0';
}
}
#include "cef/libcef_dll/cef_api_versions.inc"
@@ -91,6 +104,8 @@ CEF_EXPORT const char* cef_api_hash(int version, int entry) {
return current_version_hash->hash;
case 2:
return CEF_COMMIT_HASH;
case 3:
return CEF_SANDBOX_COMPAT_HASH;
default:
return nullptr;
}
@@ -3,6 +3,7 @@
// can be found in the LICENSE file.
#include <algorithm>
#include <cstring>
#include "include/base/cef_logging.h"
#include "include/wrapper/cef_certificate_util_win.h"
@@ -64,16 +65,71 @@ HMODULE Load(const std::wstring& dll_path,
cef_version_info_all(&dll_info);
#else
// Only populating the members that are used below.
dll_info.cef_version_major = cef_version_info(0);
dll_info.cef_version_minor = cef_version_info(1);
dll_info.cef_version_patch = cef_version_info(2);
dll_info.chrome_version_major = cef_version_info(4);
dll_info.chrome_version_patch = cef_version_info(7);
#endif
if (dll_info.chrome_version_major != version_info->chrome_version_major ||
dll_info.chrome_version_patch != version_info->chrome_version_patch) {
LOG(FATAL) << "Failed libcef.dll version check; expected "
<< version_info->chrome_version_major << "."
<< version_info->chrome_version_patch << ", got "
<< dll_info.chrome_version_major << "."
<< dll_info.chrome_version_patch;
bool check_dll_version = true;
#if CEF_API_ADDED(CEF_NEXT)
// Check sandbox compatibility hash. Only compare if both structs are
// large enough to contain the hash field.
const bool version_info_has_hash =
version_info->size >= CEF_VERSION_INFO_SIZE_WITH_SANDBOX_HASH;
const bool dll_info_has_hash =
dll_info.size >= CEF_VERSION_INFO_SIZE_WITH_SANDBOX_HASH;
if (version_info_has_hash && dll_info_has_hash) {
if (version_info->sandbox_compat_hash[0] != '\0' &&
dll_info.sandbox_compat_hash[0] != '\0') {
if (std::strcmp(version_info->sandbox_compat_hash,
dll_info.sandbox_compat_hash) != 0) {
LOG(FATAL) << "Failed libcef.dll sandbox compatibility check; "
"bootstrap hash: "
<< version_info->sandbox_compat_hash
<< ", bootstrap version: "
<< version_info->cef_version_major << "."
<< version_info->cef_version_minor << "."
<< version_info->cef_version_patch
<< ", libcef hash: " << dll_info.sandbox_compat_hash
<< ", libcef version: " << dll_info.cef_version_major
<< "." << dll_info.cef_version_minor << "."
<< dll_info.cef_version_patch;
}
check_dll_version = false;
}
}
#endif // CEF_API_ADDED(CEF_NEXT)
if (check_dll_version && (dll_info.chrome_version_major !=
version_info->chrome_version_major ||
dll_info.chrome_version_patch !=
version_info->chrome_version_patch)) {
LOG(FATAL) << "Failed libcef.dll version compatibility check; "
"bootstrap version: "
<< version_info->cef_version_major << "."
<< version_info->cef_version_minor << "."
<< version_info->cef_version_patch
<< ", libcef version: " << dll_info.cef_version_major << "."
<< dll_info.cef_version_minor << "."
<< dll_info.cef_version_patch;
}
const char* api_hash = cef_api_hash(CEF_API_VERSION, 0);
if (std::strcmp(api_hash, CEF_API_HASH_PLATFORM) != 0) {
LOG(FATAL) << "Failed libcef.dll API compatibility check; "
"API version: "
<< CEF_API_VERSION
<< ", wrapper hash: " << CEF_API_HASH_PLATFORM
<< ", wrapper version: " << CEF_VERSION_MAJOR << "."
<< CEF_VERSION_MINOR << "." << CEF_VERSION_PATCH
<< ", libcef hash: " << api_hash
<< ", libcef version: " << dll_info.cef_version_major << "."
<< dll_info.cef_version_minor << "."
<< dll_info.cef_version_patch;
}
}
+8
View File
@@ -1636,6 +1636,14 @@ if chromium_checkout != chromium_compat_version:
out_file = None
check_pattern_matches(output_file=out_file)
# On release branches, check for sandbox compat hash changes that require
# a new API version. This can happen when Chromium updates modify sandbox
# interface files.
if not branch_is_master and chromium_checkout_changed:
msg('Checking for sandbox compat hash changes...')
tool = os.path.join(cef_src_dir, 'tools', 'version_manager.py')
run('%s %s -a' % (python_exe, tool), cef_src_dir)
##
# Build CEF.
##
+125 -5
View File
@@ -24,7 +24,8 @@ else:
# "versions": [
# {
# "cef_version": "3.2704.1414.g185cd6c",
# "chromium_version": "51.0.2704.47"
# "chromium_version": "51.0.2704.47",
# "sandbox_compat": "abc123def4567890", # Optional, queried from GitHub
# "files": [
# {
# "last_modified": "2016-05-18T22:42:14.066Z"
@@ -125,6 +126,32 @@ class cef_json_builder:
return version == 'master' or \
bool(re.compile('^' + _chromium_version_regex + '$').match(version))
@staticmethod
def is_valid_sandbox_compat(sandbox_compat):
""" Returns true if the specified sandbox_compat hash is valid. """
if sandbox_compat is None:
return False
# 16-character hex string
return bool(re.compile('^[0-9a-f]{16}$').match(sandbox_compat))
@staticmethod
def cef_version_to_api_version(cef_version):
""" Convert CEF version to API version number.
E.g., '138.0.1+g...' -> '13800', '138.1.0+g...' -> '13801'
Returns None for old-style version numbers that predate API versioning.
"""
if '+' not in cef_version:
# Old style version numbers don't have API versioning.
return None
# New style: 138.0.1+g...
version_part = cef_version.split('+')[0]
parts = version_part.split('.')
chrome_major = int(parts[0])
cef_minor = int(parts[1])
return '%d%02d' % (chrome_major, cef_minor)
@staticmethod
def get_file_name(version, platform, type, channel='stable'):
""" Returns the expected distribution file name excluding extension based on
@@ -139,6 +166,8 @@ class cef_json_builder:
for platform in self.get_platforms():
self._data[platform] = {'versions': []}
self._versions = {}
self._sandbox_compats = {}
self._api_versions_cache = {}
self._queryct = 0
def filter_files(self, files, file_type, sha1, name):
@@ -246,6 +275,84 @@ class cef_json_builder:
""" Return True if a matching Chromium version is known. """
return cef_version in self._versions
def _query_api_versions_json(self, cef_version):
""" Query cef_api_versions.json from GitHub for the given CEF version. """
# Get Chromium version and extract branch (build number).
chromium_version = self.get_chromium_version(cef_version)
if not self.is_valid_chromium_version(chromium_version):
return None
parts = chromium_version.split('.')
branch = parts[2] # Chromium build number (e.g., 7204 from 138.0.7204.0)
# Check cache first.
if branch in self._api_versions_cache:
return self._api_versions_cache[branch]
query_url = 'https://raw.githubusercontent.com/chromiumembedded/cef/%s/cef_api_versions.json' % branch
self._queryct = self._queryct + 1
if not self._silent:
print('Reading %s' % query_url)
try:
handle = urlopen(query_url)
json_data = json.loads(handle.read().decode('utf-8'))
handle.close()
self._api_versions_cache[branch] = json_data
return json_data
except Exception as e:
if not self._silent:
print('Failed to read cef_api_versions.json: %s' % e)
return None
def _query_sandbox_compat(self, cef_version):
""" Query sandbox_compat from cef_api_versions.json on GitHub. """
target_version = self.cef_version_to_api_version(cef_version)
if target_version is None:
# Old-style version numbers don't have API versioning.
return None
json_data = self._query_api_versions_json(cef_version)
if json_data is None or 'hashes' not in json_data:
return None
# Find exact match or closest lower version.
available = sorted(json_data['hashes'].keys(), reverse=True)
for version in available:
if version <= target_version:
return json_data['hashes'][version].get('sandbox_compat')
return None
def set_sandbox_compat(self, cef_version, sandbox_compat=None):
""" Set the sandbox_compat hash for the given CEF version. If not provided
or invalid, it will be queried remotely. """
if not self.is_valid_version(cef_version):
raise Exception('Invalid CEF version: %s' % cef_version)
if not self.is_valid_sandbox_compat(sandbox_compat):
if cef_version in self._sandbox_compats:
# Keep the sandbox_compat that we already know about.
return self._sandbox_compats[cef_version]
sandbox_compat = self._query_sandbox_compat(cef_version)
if sandbox_compat is not None:
self._sandbox_compats[cef_version] = sandbox_compat
return sandbox_compat
def get_sandbox_compat(self, cef_version):
""" Return the sandbox_compat hash. If not currently known it will be
queried remotely. May return None if not available. """
if cef_version in self._sandbox_compats:
return self._sandbox_compats[cef_version]
return self.set_sandbox_compat(cef_version)
def has_sandbox_compat(self, cef_version):
""" Return True if a sandbox_compat hash is known. """
return cef_version in self._sandbox_compats
def load(self, json_string, fatalerrors=True):
""" Load new JSON into this object. Any existing contents will be cleared.
If |fatalerrors| is True then any errors while loading the JSON file
@@ -311,7 +418,7 @@ class cef_json_builder:
})
if len(valid_files) > 0:
valid_versions.append({
version_record = {
'cef_version':
version['cef_version'],
'chromium_version':
@@ -321,7 +428,13 @@ class cef_json_builder:
version.get('channel', 'stable'),
'files':
self._sort_files(valid_files)
})
}
# Include sandbox_compat if available.
sandbox_compat = self.set_sandbox_compat(
version['cef_version'], version.get('sandbox_compat'))
if sandbox_compat is not None:
version_record['sandbox_compat'] = sandbox_compat
valid_versions.append(version_record)
if len(valid_versions) > 0:
self._data[platform]['versions'] = valid_versions
@@ -447,12 +560,17 @@ class cef_json_builder:
if version_idx == -1:
# Add a new version record.
self._print('add_file: Add %s %s %s' % (platform, version, channel))
self._data[platform]['versions'].append({
version_record = {
'cef_version': version,
'chromium_version': self.get_chromium_version(version),
'channel': channel,
'files': []
})
}
# Include sandbox_compat if available.
sandbox_compat = self.get_sandbox_compat(version)
if sandbox_compat is not None:
version_record['sandbox_compat'] = sandbox_compat
self._data[platform]['versions'].append(version_record)
version_idx = len(self._data[platform]['versions']) - 1
files = self._data[platform]['versions'][version_idx]['files']
@@ -498,6 +616,8 @@ class cef_json_builder:
result_obj['cef_version'] = version_obj['cef_version']
result_obj['chromium_version'] = version_obj['chromium_version']
result_obj['channel'] = version_obj['channel']
if 'sandbox_compat' in version_obj:
result_obj['sandbox_compat'] = version_obj['sandbox_compat']
results.append(result_obj)
return results
+270 -1
View File
@@ -5,7 +5,9 @@
from __future__ import absolute_import
from cef_json_builder import cef_json_builder
import datetime
import json
import unittest
from unittest.mock import patch, MagicMock
class TestCefJSONBuilder(unittest.TestCase):
@@ -100,6 +102,10 @@ class TestCefJSONBuilder(unittest.TestCase):
chromium_version = '49.0.2705.50'
self.assertEqual(chromium_version,
builder.set_chromium_version(version, chromium_version))
# For new-style versions, set a dummy sandbox_compat to avoid GitHub queries.
# Old-style versions don't have sandbox_compat.
if '+' in version:
builder.set_sandbox_compat(version, 'abc123def4567890')
self.assertEqual(0, builder.get_query_count())
result = builder.add_file(name, attribs[attrib_idx]['size'],
@@ -109,7 +115,7 @@ class TestCefJSONBuilder(unittest.TestCase):
self.assertEqual(not shouldfail, result)
# Return the result expected from get_files().
return {
expected = {
'chromium_version': chromium_version,
'sha1': attribs[attrib_idx]['sha1'],
'name': name,
@@ -120,6 +126,10 @@ class TestCefJSONBuilder(unittest.TestCase):
'type': type,
'size': attribs[attrib_idx]['size']
}
# Include sandbox_compat only for new-style versions.
if '+' in version:
expected['sandbox_compat'] = 'abc123def4567890'
return expected
# Test with no file contents.
def test_empty(self):
@@ -457,6 +467,265 @@ class TestCefJSONBuilder(unittest.TestCase):
files = builder.get_files()
self.assertEqual(len(files), 2)
# Test sandbox_compat hash validation.
def test_valid_sandbox_compat(self):
# Valid 16-character hex string.
self.assertTrue(
cef_json_builder.is_valid_sandbox_compat('0123456789abcdef'))
self.assertTrue(
cef_json_builder.is_valid_sandbox_compat('abcdef0123456789'))
# Invalid values.
self.assertFalse(cef_json_builder.is_valid_sandbox_compat(None))
self.assertFalse(cef_json_builder.is_valid_sandbox_compat(''))
self.assertFalse(cef_json_builder.is_valid_sandbox_compat('foobar'))
# Too short.
self.assertFalse(
cef_json_builder.is_valid_sandbox_compat('0123456789abcde'))
# Too long.
self.assertFalse(
cef_json_builder.is_valid_sandbox_compat('0123456789abcdef0'))
# Invalid characters.
self.assertFalse(
cef_json_builder.is_valid_sandbox_compat('0123456789abcdeg'))
# Uppercase not allowed.
self.assertFalse(
cef_json_builder.is_valid_sandbox_compat('0123456789ABCDEF'))
# Test CEF version to API version conversion.
def test_cef_version_to_api_version(self):
# New-style version numbers.
self.assertEqual('13800',
cef_json_builder.cef_version_to_api_version(
'138.0.1+g62d140e+chromium-138.0.7204.0'))
self.assertEqual('13801',
cef_json_builder.cef_version_to_api_version(
'138.1.0+g62d140e+chromium-138.0.7204.0'))
self.assertEqual('13802',
cef_json_builder.cef_version_to_api_version(
'138.2.5+g62d140e+chromium-138.0.7204.0'))
self.assertEqual('7401',
cef_json_builder.cef_version_to_api_version(
'74.1.0+g62d140e+chromium-74.0.3729.6'))
self.assertEqual('10000',
cef_json_builder.cef_version_to_api_version(
'100.0.0+g62d140e+chromium-100.0.4896.0'))
# Old-style version numbers return None (no API versioning).
self.assertIsNone(
cef_json_builder.cef_version_to_api_version('3.2704.1414.g185cd6c'))
self.assertIsNone(
cef_json_builder.cef_version_to_api_version('3.2623.9999.gb90a3be'))
# Helper to create mock API versions JSON response.
def _create_mock_api_versions_json(self, versions_with_sandbox):
"""Create a mock cef_api_versions.json structure."""
hashes = {}
for version, sandbox_compat in versions_with_sandbox:
hashes[version] = {
'comment': 'Test version.',
'linux': 'a' * 40,
'mac': 'b' * 40,
'windows': 'c' * 40,
}
if sandbox_compat:
hashes[version]['sandbox_compat'] = sandbox_compat
return {'hashes': hashes, 'min': min(versions_with_sandbox)[0]}
# Test sandbox_compat query with mocked GitHub response.
@patch('cef_json_builder.urlopen')
def test_sandbox_compat_query(self, mock_urlopen):
builder = cef_json_builder(silent=True)
# Mock API versions JSON with sandbox_compat.
mock_json = self._create_mock_api_versions_json([
('13800', 'abc123def4567890'),
('13801', 'def456abc7890123'),
])
mock_response = MagicMock()
mock_response.read.return_value = json.dumps(mock_json).encode('utf-8')
mock_urlopen.return_value = mock_response
cef_version = '138.0.1+g62d140e+chromium-138.0.7204.0'
# Test query.
self.assertFalse(builder.has_sandbox_compat(cef_version))
sandbox_compat = builder.get_sandbox_compat(cef_version)
self.assertEqual('abc123def4567890', sandbox_compat)
self.assertTrue(builder.has_sandbox_compat(cef_version))
# Verify URL was called with branch number from Chromium version.
mock_urlopen.assert_called_once()
call_url = mock_urlopen.call_args[0][0]
self.assertIn('/7204/', call_url) # Branch from chromium-138.0.7204.0
self.assertIn('cef_api_versions.json', call_url)
# Test sandbox_compat falls back to closest lower version.
@patch('cef_json_builder.urlopen')
def test_sandbox_compat_fallback(self, mock_urlopen):
builder = cef_json_builder(silent=True)
# Only 13800 exists, not 13801.
mock_json = self._create_mock_api_versions_json([
('13800', 'abc123def4567890'),
])
mock_response = MagicMock()
mock_response.read.return_value = json.dumps(mock_json).encode('utf-8')
mock_urlopen.return_value = mock_response
# CEF version 138.1.x should fall back to 13800.
cef_version = '138.1.0+g62d140e+chromium-138.0.7204.0'
sandbox_compat = builder.get_sandbox_compat(cef_version)
self.assertEqual('abc123def4567890', sandbox_compat)
# Test sandbox_compat returns None when not available.
@patch('cef_json_builder.urlopen')
def test_sandbox_compat_not_available(self, mock_urlopen):
builder = cef_json_builder(silent=True)
# Version exists but has no sandbox_compat field.
mock_json = self._create_mock_api_versions_json([
('13800', None),
])
mock_response = MagicMock()
mock_response.read.return_value = json.dumps(mock_json).encode('utf-8')
mock_urlopen.return_value = mock_response
cef_version = '138.0.1+g62d140e+chromium-138.0.7204.0'
sandbox_compat = builder.get_sandbox_compat(cef_version)
self.assertIsNone(sandbox_compat)
# Test sandbox_compat handles query failure gracefully.
@patch('cef_json_builder.urlopen')
def test_sandbox_compat_query_failure(self, mock_urlopen):
builder = cef_json_builder(silent=True)
# Simulate network error.
mock_urlopen.side_effect = Exception('Network error')
cef_version = '138.0.1+g62d140e+chromium-138.0.7204.0'
sandbox_compat = builder.get_sandbox_compat(cef_version)
self.assertIsNone(sandbox_compat)
# Test sandbox_compat returns None when target version is older than all available.
@patch('cef_json_builder.urlopen')
def test_sandbox_compat_no_matching_version(self, mock_urlopen):
builder = cef_json_builder(silent=True)
# Only newer versions available (13900, 14000), target is 13800.
mock_json = self._create_mock_api_versions_json([
('13900', 'abc123def4567890'),
('14000', 'def456abc7890123'),
])
mock_response = MagicMock()
mock_response.read.return_value = json.dumps(mock_json).encode('utf-8')
mock_urlopen.return_value = mock_response
# CEF 138.0.x = API version 13800, which is older than available.
cef_version = '138.0.1+g62d140e+chromium-138.0.7204.0'
sandbox_compat = builder.get_sandbox_compat(cef_version)
self.assertIsNone(sandbox_compat)
# Test sandbox_compat returns None when fallback version lacks the field.
@patch('cef_json_builder.urlopen')
def test_sandbox_compat_fallback_no_field(self, mock_urlopen):
builder = cef_json_builder(silent=True)
# 13800 exists but has no sandbox_compat field.
mock_json = self._create_mock_api_versions_json([
('13800', None),
])
mock_response = MagicMock()
mock_response.read.return_value = json.dumps(mock_json).encode('utf-8')
mock_urlopen.return_value = mock_response
# CEF 138.1.x = API version 13801, falls back to 13800 which lacks field.
cef_version = '138.1.0+g62d140e+chromium-138.0.7204.0'
sandbox_compat = builder.get_sandbox_compat(cef_version)
self.assertIsNone(sandbox_compat)
# Test sandbox_compat is included in version records.
@patch('cef_json_builder.urlopen')
def test_sandbox_compat_in_version_record(self, mock_urlopen):
builder = cef_json_builder(silent=True)
# Mock API versions JSON.
mock_json = self._create_mock_api_versions_json([
('13800', 'abc123def4567890'),
])
mock_response = MagicMock()
mock_response.read.return_value = json.dumps(mock_json).encode('utf-8')
mock_urlopen.return_value = mock_response
cef_version = '138.0.1+g62d140e+chromium-138.0.7204.0'
chromium_version = '138.0.7204.0'
# Pre-populate chromium version to avoid additional queries.
builder.set_chromium_version(cef_version, chromium_version)
# Add a file.
name = cef_json_builder.get_file_name(cef_version, 'linux64',
'standard') + '.tar.gz'
builder.add_file(name, 12345, '2026-02-10T12:00:00.000Z', 'a' * 40)
# Verify sandbox_compat is in the version record.
versions = builder.get_versions('linux64')
self.assertEqual(1, len(versions))
self.assertEqual('abc123def4567890', versions[0].get('sandbox_compat'))
# Verify sandbox_compat is in get_files() result.
files = builder.get_files()
self.assertEqual(1, len(files))
self.assertEqual('abc123def4567890', files[0].get('sandbox_compat'))
# Test sandbox_compat is preserved through load/save cycle.
@patch('cef_json_builder.urlopen')
def test_sandbox_compat_load_save(self, mock_urlopen):
builder = cef_json_builder(silent=True)
# Mock API versions JSON.
mock_json = self._create_mock_api_versions_json([
('13800', 'abc123def4567890'),
])
mock_response = MagicMock()
mock_response.read.return_value = json.dumps(mock_json).encode('utf-8')
mock_urlopen.return_value = mock_response
cef_version = '138.0.1+g62d140e+chromium-138.0.7204.0'
chromium_version = '138.0.7204.0'
builder.set_chromium_version(cef_version, chromium_version)
name = cef_json_builder.get_file_name(cef_version, 'linux64',
'standard') + '.tar.gz'
builder.add_file(name, 12345, '2026-02-10T12:00:00.000Z', 'a' * 40)
# Save to string.
output = str(builder)
self.assertIn('sandbox_compat', output)
self.assertIn('abc123def4567890', output)
# Load into new builder.
builder2 = cef_json_builder(silent=True)
builder2.load(output)
# Verify sandbox_compat is preserved.
versions = builder2.get_versions('linux64')
self.assertEqual(1, len(versions))
self.assertEqual('abc123def4567890', versions[0].get('sandbox_compat'))
# Test set_sandbox_compat with explicit value.
def test_set_sandbox_compat_explicit(self):
builder = cef_json_builder(silent=True)
cef_version = '138.0.1+g62d140e+chromium-138.0.7204.0'
# Set explicit value (no query needed).
sandbox_compat = builder.set_sandbox_compat(cef_version, '1234567890abcdef')
self.assertEqual('1234567890abcdef', sandbox_compat)
self.assertTrue(builder.has_sandbox_compat(cef_version))
self.assertEqual(0, builder.get_query_count())
# Program entry point.
if __name__ == '__main__':
+25 -5
View File
@@ -7,11 +7,12 @@ from cef_parser import get_copyright
from cef_version import VersionFormatter
from date_util import *
from file_util import *
from sandbox_compat_hash import calculate_sandbox_compat_hash
import git_util as git
import sys
def make_version_header(header):
def make_version_header(header, sandbox_files=None):
result = get_copyright(full=True, translator=False) + \
"""//
// ---------------------------------------------------------------------------
@@ -35,6 +36,8 @@ def make_version_header(header):
#define CHROME_VERSION_BUILD $CHROME_BUILD$
#define CHROME_VERSION_PATCH $CHROME_PATCH$
#define CEF_SANDBOX_COMPAT_HASH "$SANDBOX_COMPAT_HASH$"
#define DO_MAKE_STRING(p) #p
#define MAKE_STRING(p) DO_MAKE_STRING(p)
@@ -60,19 +63,36 @@ def make_version_header(header):
result = result.replace('$CHROME_%s$' % key,
str(chrome_version_components[key]))
# Calculate sandbox compat hash (Windows only, empty on other platforms)
if sandbox_files:
sandbox_hash = calculate_sandbox_compat_hash(sandbox_files)
else:
sandbox_hash = ""
result = result.replace('$SANDBOX_COMPAT_HASH$', sandbox_hash)
return result
def write_version_header(output):
result = make_version_header(output)
def write_version_header(output, sandbox_files=None):
result = make_version_header(output, sandbox_files)
return write_file_if_changed(output, result)
def main(argv):
if len(argv) < 2:
print(("Usage:\n %s <output_filename>" % argv[0]))
print((
"Usage:\n %s <output_filename> [--sandbox-files file1 ...]" % argv[0]))
sys.exit(-1)
write_version_header(argv[1])
output = argv[1]
sandbox_files = []
# Check for --sandbox-files flag
if '--sandbox-files' in argv:
idx = argv.index('--sandbox-files')
sandbox_files = argv[idx + 1:]
write_version_header(output, sandbox_files)
if '__main__' == __name__:
+44
View File
@@ -0,0 +1,44 @@
# Copyright (c) 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.
from __future__ import absolute_import
import hashlib
import os
import sys
def calculate_sandbox_compat_hash(file_paths, verbose=False):
"""
Calculate a compatibility hash from the contents of the given files.
Returns a truncated SHA-256 hash (first 16 hex characters).
All files must exist or an error is raised.
"""
hasher = hashlib.sha256()
for file_path in sorted(file_paths): # Sort for deterministic order
if not os.path.exists(file_path):
raise FileNotFoundError(f"Required file not found: {file_path}")
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
content = f.read()
# Normalize line endings for cross-platform consistency
content = content.replace('\r\n', '\n')
hasher.update(content.encode('utf-8'))
if verbose:
print(f"Hashed: {file_path} ({len(content)} bytes)")
return hasher.hexdigest()[:16]
def main(argv):
if len(argv) < 2:
print(f"Usage: {argv[0]} <file1> [file2] ...")
sys.exit(1)
hash_value = calculate_sandbox_compat_hash(argv[1:], verbose=True)
print(f"Sandbox compatibility hash: {hash_value}")
if __name__ == '__main__':
main(sys.argv)
+61 -15
View File
@@ -12,9 +12,19 @@ from file_util import read_file, read_json_file, write_file, write_json_file
from git_util import exec_git_cmd
import os
import sys
from sandbox_compat_hash import calculate_sandbox_compat_hash
from translator import translate
from version_util import *
# Sandbox interface files for compatibility hash (paths relative to src/).
# Keep in sync with _sandbox_compat_files in cef/BUILD.gn.
SANDBOX_COMPAT_FILES = [
'chrome/chrome_elf/chrome_elf_main.h',
'sandbox/policy/mojom/sandbox.mojom',
'sandbox/win/src/sandbox.h',
'sandbox/win/src/sandbox_types.h',
]
def get_next_api_revision(api_versions_file, major_version):
""" Returns the next available API revision for |major_version|.
@@ -44,7 +54,8 @@ def get_next_api_revision(api_versions_file, major_version):
def compute_api_hashes(api_version: str,
hasher: CefApiHasher,
next_allowed: bool) -> Dict[str, str]:
next_allowed: bool,
src_dir: str) -> Dict[str, str]:
""" Computes API hashes for the specified |api_version|. """
if not next_allowed:
# Next usage is banned with explicit API versions.
@@ -58,6 +69,10 @@ def compute_api_hashes(api_version: str,
hashes = hasher.calculate(api_version, added_defines)
if hashes:
# Add sandbox compatibility hash.
sandbox_files = [os.path.join(src_dir, f) for f in SANDBOX_COMPAT_FILES]
hashes['sandbox_compat'] = calculate_sandbox_compat_hash(sandbox_files)
if api_version in UNTRACKED_VERSIONS:
label = version_label(api_version)
label = label[0:1].upper() + label[1:]
@@ -67,9 +82,24 @@ def compute_api_hashes(api_version: str,
return hashes
def same_api_hashes(hashes1, hashes2):
return all(hashes1[key] == hashes2[key]
for key in ['linux', 'mac', 'windows'])
def same_api_hashes(hashes1, hashes2, compare_sandbox=True):
# Compare platform hashes (always required).
for key in ['linux', 'mac', 'windows']:
if hashes1.get(key) != hashes2.get(key):
return False
# Compare sandbox_compat only when requested.
if compare_sandbox:
has1 = 'sandbox_compat' in hashes1
has2 = 'sandbox_compat' in hashes2
if has1 and has2:
# Both have the field - compare values.
if hashes1['sandbox_compat'] != hashes2['sandbox_compat']:
return False
elif has1 or has2:
# One has the field and the other doesn't - not the same.
# This ensures the field gets added to entries that lack it.
return False
return True
def compute_next_api_version(api_versions_file):
@@ -200,7 +230,8 @@ def exec_apply(api_versions_file,
api_untracked_file,
next_version,
apply_next,
hasher: CefApiHasher) -> int:
hasher: CefApiHasher,
src_dir: str) -> int:
""" Updates untracked API hashes if necessary.
Saves the hash for the next API version if |apply_next| is true.
"""
@@ -214,13 +245,14 @@ def exec_apply(api_versions_file,
untracked_changed = False
for version in UNTRACKED_VERSIONS:
label = version_label(version)
hashes = compute_api_hashes(version, hasher, next_allowed=True)
hashes = compute_api_hashes(
version, hasher, next_allowed=True, src_dir=src_dir)
if not hashes:
sys.stderr.write('ERROR: Failed to process %s\n' % label)
return 1
if version in json_untracked['hashes'] and same_api_hashes(
hashes, json_untracked['hashes'][version]):
hashes, json_untracked['hashes'][version], compare_sandbox=True):
print('Hashes for %s are unchanged.' % label)
else:
untracked_changed = True
@@ -231,14 +263,20 @@ def exec_apply(api_versions_file,
if apply_next:
next_label = version_label(next_version)
hashes = compute_api_hashes(next_version, hasher, next_allowed=False)
hashes = compute_api_hashes(
next_version, hasher, next_allowed=False, src_dir=src_dir)
if not hashes:
sys.stderr.write('ERROR: Failed to process %s\n' % next_label)
return 1
last_version = json_versions.get('last', None)
if not last_version is None and last_version in json_versions['hashes']:
if same_api_hashes(hashes, json_versions['hashes'][last_version]):
# Compare sandbox_compat_hash if both have it. For older versions that
# don't have the field, the comparison is skipped (handled by
# same_api_hashes). Once last_version has the field, sandbox changes
# will trigger new version bumps.
if same_api_hashes(
hashes, json_versions['hashes'][last_version], compare_sandbox=True):
print('Hashes for last %s are unchanged.' % version_label(last_version))
next_changed = False
@@ -248,7 +286,8 @@ def exec_apply(api_versions_file,
json_versions['hashes'][next_version] = hashes
if NEXT_VERSION in json_untracked['hashes'] and not \
same_api_hashes(hashes, json_untracked['hashes'][NEXT_VERSION]):
same_api_hashes(hashes, json_untracked['hashes'][NEXT_VERSION],
compare_sandbox=True):
print('NOTE: Additional versions are available to generate.')
write_versions = next_changed or not os.path.isfile(api_versions_file)
@@ -273,7 +312,8 @@ def exec_check(api_versions_file,
fast_check,
force_update,
skip_untracked,
hasher: CefApiHasher) -> int:
hasher: CefApiHasher,
src_dir: str) -> int:
""" Checks existing API version hashes.
Resaves all API hashes if |force_update| is true. Otherwise, hash
changes are considered an error.
@@ -325,11 +365,16 @@ def exec_check(api_versions_file,
else:
stored_hashes = json_versions['hashes'][version]
label = version_label(version)
computed_hashes = compute_api_hashes(version, hasher, next_allowed=True)
computed_hashes = compute_api_hashes(
version, hasher, next_allowed=True, src_dir=src_dir)
if not bool(computed_hashes):
sys.stderr.write('ERROR: Failed to process %s\n' % label)
return 1
if not same_api_hashes(computed_hashes, stored_hashes):
# Only compare sandbox_compat_hash for untracked versions since we can
# only compute the hash for the current file state, not for historical
# versions.
if not same_api_hashes(
computed_hashes, stored_hashes, compare_sandbox=untracked):
if force_update:
print('Updating hashes for %s' % label)
if untracked:
@@ -477,6 +522,7 @@ https://chromiumembedded.github.io/cef/api_versioning
script_dir = os.path.dirname(__file__)
cef_dir = os.path.abspath(os.path.join(script_dir, os.pardir))
src_dir = os.path.abspath(os.path.join(cef_dir, os.pardir))
cpp_header_dir = os.path.join(cef_dir, 'include')
if not os.path.isdir(cpp_header_dir):
@@ -526,7 +572,7 @@ https://chromiumembedded.github.io/cef/api_versioning
api_untracked_file):
skip_untracked = True
if exec_apply(api_versions_file, api_untracked_file, next_version,
options.apply, hasher) > 0:
options.apply, hasher, src_dir) > 0:
# Apply failed.
sys.exit(1)
elif not options.check:
@@ -536,4 +582,4 @@ https://chromiumembedded.github.io/cef/api_versioning
sys.exit(
exec_check(api_versions_file, api_untracked_file, options.fastcheck and
not options.force_update, options.check and
options.force_update, skip_untracked, hasher))
options.force_update, skip_untracked, hasher, src_dir))