背景
在 Chromium 的设置页面(chrome://settings)中,前端通过 chrome.settingsPrivate.setPref() API 来修改浏览器配置项(Pref)。本文以一个真实 Bug 为切入点,深度剖析这套机制的底层原理。
Bug 现象:前端调用 settingsPrivate.setPref('performance_tuning.high_efficiency_mode.aggressiveness', value) 后,C++ 层的 SettingsPrivateSetPrefFunction::Run() 断点没有触发,前端也没有收到任何回调。
一、整体架构:三层设计
Chromium 的设置系统由三层组成:
前端 WebUI (TypeScript/HTML)
↓ chrome.settingsPrivate.setPref()
Extension API 层 (C++)
↓ PrefsUtil::SetPref()
PrefService 层 (C++)
1.1 前端层
设置页面是一个 WebUI,运行在 chrome://settings。前端通过 chrome.settingsPrivate 这个 Extension API 与 C++ 通信:
// 前端调用示例
chrome.settingsPrivate.setPref(
'performance_tuning.high_efficiency_mode.aggressiveness',
2 // kAggressive
);
1.2 Extension API 层
入口在 chrome/browser/extensions/api/settings_private/settings_private_api.cc:
ExtensionFunction::ResponseAction SettingsPrivateSetPrefFunction::Run() {
auto parameters = api::settings_private::SetPref::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(parameters);
SettingsPrivateDelegate* delegate =
SettingsPrivateDelegateFactory::GetForBrowserContext(browser_context());
settings_private::SetPrefResult result =
delegate->SetPref(parameters->name, ¶meters->value);
// ...
}
1.3 PrefService 层
Chromium 的配置系统核心,负责持久化存储和读取所有配置项。
二、关键机制:Allowlist(白名单)
settings_private 不是一个万能的 Pref 读写接口。它有严格的白名单机制,只有在 GetAllowlistedKeys() 中注册的 Pref 才能被前端访问。
文件位置:chrome/browser/extensions/api/settings_private/prefs_util.cc
const PrefsUtil::TypedPrefMap& PrefsUtil::GetAllowlistedKeys() {
static PrefsUtil::TypedPrefMap* s_allowlist = nullptr;
if (s_allowlist) {
return *s_allowlist;
}
s_allowlist = new PrefsUtil::TypedPrefMap();
// Performance settings.
(*s_allowlist)
[performance_manager::user_tuning::prefs::kMemorySaverModeState] =
settings_api::PrefType::kNumber;
(*s_allowlist)[performance_manager::user_tuning::prefs::
kMemorySaverModeAggressiveness] =
settings_api::PrefType::kNumber;
// ...
return *s_allowlist;
}
白名单的作用:
- 安全隔离:防止恶意扩展或页面随意修改任意 Pref
- 类型声明:声明每个 Pref 的预期类型(kNumber/kBoolean/kString 等)
- 变更监听:
settings_private_event_router只监听白名单内的 Pref 变化
三、SetPref 完整调用链
chrome.settingsPrivate.setPref(name, value)
→ SettingsPrivateSetPrefFunction::Run()
→ SettingsPrivateDelegate::SetPref()
→ PrefsUtil::SetPref()
① GetAllowlistedPrefType(pref_name) // 检查白名单
② FindServiceForPref(pref_name) // 找到对应的 PrefService
③ IsPrefUserModifiable(pref_name) // 检查是否可修改
④ pref_service->FindPreference(pref_name) // 关键:查找 Pref 是否存在
⑤ pref_service->SetInteger(pref_name, value) // 实际写入
3.1 步骤①:白名单检查
if (GetAllowlistedPrefType(pref_name) == settings_api::PrefType::kNone) {
return settings_private::SetPrefResult::PREF_NOT_FOUND;
}
如果 Pref 不在白名单里,直接返回 PREF_NOT_FOUND。
3.2 步骤②:FindServiceForPref
PrefService* PrefsUtil::FindServiceForPref(const std::string& pref_name) {
PrefService* user_prefs = profile_->GetPrefs();
// 优先查 profile prefs
if (user_prefs->FindPreference(pref_name)) {
return user_prefs;
}
// 再查 local_state
if (g_browser_process->local_state()->FindPreference(pref_name)) {
return g_browser_process->local_state();
}
// 默认返回 user_prefs(此时 FindPreference 会返回 null)
return user_prefs;
}
这里有个关键点:如果 Pref 没有被注册,两个 FindPreference 都返回 null,函数默认返回 user_prefs。
3.3 步骤④:致命的 FindPreference
const PrefService::Preference* pref = pref_service->FindPreference(pref_name);
if (!pref) {
return settings_private::SetPrefResult::PREF_NOT_FOUND;
}
如果 Pref 没有被注册到任何 PrefService,这里就会返回 null,整个调用返回 PREF_NOT_FOUND。
四、PrefService 的两种存储:local_state vs profile prefs
Chromium 有两个主要的 PrefService 实例:
| 特性 | local_state | profile prefs |
|---|---|---|
| 作用域 | 全局,所有用户共享 | 每个用户 Profile 独立 |
| 注册函数 | RegisterLocalStatePrefs() | RegisterProfilePrefs() |
| 存储位置 | Local State 文件 | Preferences 文件 |
| 典型用途 | 性能设置、系统级配置 | 用户偏好、书签、历史 |
文件位置:components/performance_manager/user_tuning/prefs.cc
void RegisterLocalStatePrefs(PrefRegistrySimple* registry) {
registry->RegisterIntegerPref(
kMemorySaverModeState,
static_cast<int>(MemorySaverModeState::kDisabled));
#ifdef USE_360HACK
registry->RegisterIntegerPref(
kMemorySaverModeAggressiveness,
static_cast<int>(MemorySaverModeAggressiveness::kMedium));
#endif
// ...
}
注意:kMemorySaverModeAggressiveness 被包在 #ifdef USE_360HACK 里!如果这个宏没有定义,这个 Pref 就不会被注册。
五、根因分析:Pref 注册 vs Allowlist 的区别
这是本次 Bug 的核心认知:
Allowlist(白名单)= 权限名单
PrefRegistry(注册)= 户口本
有权限但没上户口,API 也改不了。
具体来说:
- Allowlist 只是告诉
settings_private系统"这个 Pref 允许被前端访问" - PrefRegistry 才是真正让 Pref 在 PrefService 中"存在"的操作
两者缺一不可:
情况1:在 Allowlist,也在 Registry → ✅ 正常工作
情况2:在 Allowlist,不在 Registry → ❌ PREF_NOT_FOUND(本次 Bug)
情况3:不在 Allowlist,在 Registry → ❌ PREF_NOT_FOUND(第一道关卡就拦截)
情况4:都不在 → ❌ PREF_NOT_FOUND
六、变更通知机制:settings_private_event_router
当 Pref 被成功修改后,前端如何收到通知?
文件位置:chrome/browser/extensions/api/settings_private/settings_private_event_router.cc
// 启动时,对白名单中所有 Pref 注册监听
const PrefsUtil::TypedPrefMap& keys = prefs_util_->GetAllowlistedKeys();
for (const auto& it : keys) {
// 找到对应的 PrefChangeRegistrar
PrefChangeRegistrar* registrar = FindRegistrarForPref(it.first);
if (registrar) {
registrar->Add(it.first, base::BindRepeating(
&SettingsPrivateEventRouter::OnPreferenceChanged, ...));
}
}
当 Pref 值变化时,事件路由器会触发 onPrefsChanged 事件,前端监听这个事件来更新 UI:
chrome.settingsPrivate.onPrefsChanged.addListener((prefs) => {
// 更新 UI
});
如果 Pref 没有注册,FindRegistrarForPref 也找不到对应的 registrar,监听就不会建立,前端自然收不到变更通知。
七、完整的 Bug 复现路径
1. 前端点击"最大限度"单选按钮
2. 触发 sendPrefChange()
3. 调用 chrome.settingsPrivate.setPref(
'performance_tuning.high_efficiency_mode.aggressiveness', 2)
4. C++ 收到请求,进入 SettingsPrivateSetPrefFunction::Run()
↓ 断点在这里,但如果前端根本没发请求,这里就不会触发
5. PrefsUtil::SetPref() 被调用
6. GetAllowlistedPrefType() → kNumber(白名单有)
7. FindServiceForPref() → 两个 FindPreference 都返回 null → 返回 user_prefs
8. pref_service->FindPreference(pref_name) → null(没注册!)
9. 返回 PREF_NOT_FOUND
10. 前端收到 false 或 error,UI 状态不更新
八、修复方案
确保 kMemorySaverModeAggressiveness 被正确注册:
// components/performance_manager/user_tuning/prefs.cc
void RegisterLocalStatePrefs(PrefRegistrySimple* registry) {
// ...
registry->RegisterIntegerPref(
kMemorySaverModeAggressiveness,
static_cast<int>(MemorySaverModeAggressiveness::kMedium));
// ...
}
或者如果它应该是 Profile 级别的(跟随用户):
void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) {
// ...
registry->RegisterIntegerPref(
kMemorySaverModeAggressiveness,
static_cast<int>(MemorySaverModeAggressiveness::kMedium));
}
九、总结:新增一个 settings_private 可访问的 Pref 的完整步骤
-
定义 Pref 名称常量(
prefs.h):inline constexpr char kMyNewPref[] = "my.new.pref"; -
注册 Pref(
prefs.cc):// 根据作用域选择注册位置 registry->RegisterIntegerPref(kMyNewPref, default_value); -
加入 Allowlist(
prefs_util.cc):(*s_allowlist)[my_component::prefs::kMyNewPref] = settings_api::PrefType::kNumber; -
前端使用:
chrome.settingsPrivate.setPref('my.new.pref', value); chrome.settingsPrivate.getPref('my.new.pref', (pref) => { ... });
缺少任何一步都会导致 API 调用失败。 这就是本次 Bug 的根本原因——步骤 2 在特定编译条件下被跳过了。
795



被折叠的 条评论
为什么被折叠?



