本文基于定制浏览器(Chromium 148 分支)与旧版(132 分支)的对比排查整理。
背景
我们维护的浏览器基于 Chromium,并有一套独立的云同步体系——密码并不走 Google/Chromium Sync,而是由客户端把整份 Login Data SQLite 文件加密后上传,登录后再下载、合并、切库。
132 分支上,用户登录云账号后,密码管理页能正常看到云端条目;升级到 148 内核 后,同步流程表面成功,但密码列表始终为空。手动刷新、重开设置页均无效。
最初怀疑是前端或 WebUI 回归,对比 132/148 的 chrome://password-manager 代码后发现:列表加载逻辑完全一致(getCredentialGroups → passwordsPrivate → SavedPasswordsPresenter)。问题出在更底层的「写库 / 切库」链路上。
密码云同步在做什么(简版)
登录
→ 切换到 profile/<用户哈希>/Login Data
→ 下载云端加密 blob
→ 写入 Sync Login Data.tmp
→ 复制本地 Login Data → Login Data.tmp
→ Merge:读云端库 → AES 解密 → OSCrypt 加密写回 tmp
→ CopyAndSwitchDBFile:Login Data.tmp → 正式 Login Data
→ PasswordStore / UI 读正式库
UI 侧 C++ 入口是 Extension API getCredentialGroups(),底层异步调用 LoginDatabaseAsyncHelper::GetAllLogins()。库里有行,UI 才有行;库空,前端怎么刷新都没用。
148 内核带来了什么变化
148 相对 132,至少有三处上游变动,与本次故障直接相关:
1. sql/ 层:catalog 表从 sqlite_master 改为 sqlite_schema
Chromium 148 的 sql/database.cc 统一用 sqlite_schema 做 schema introspection(DoesTableExist、错误诊断、integrity 等)。该视图在 SQLite ≥ 3.33 才作为 sqlite_master 的别名提供。
我们的发行版仍链接 SQLite 3.31.1(含 SQLITE_HAS_CODEC 等定制 amalgamation),只有 sqlite_master。未适配时,MetaTable::Init 等路径会报:
no such table: sqlite_schema
LoginDatabase::Init 失败,merge 里的 new_db / cur_db 无法正常打开。
132 里部分代码也写了 sqlite_schema,在 3.31.1 上属于「侥幸能跑」——prepare 失败时被当成「表不存在」,走 CREATE 路径;对已有表的云端库容易误判。148 把问题放大、暴露得更彻底。
修复思路:在定制编译宏下,将 catalog 宏映射回 sqlite_master,与绑定的 SQLite 版本对齐。
2. 密码库加密:os_crypt_async 成为主路径,standalone DB 无 encryptor
148 的 LoginDatabase::EncryptedString(Windows)大致变为:
// 148 上游(简化)
bool result = encryptor_ && encryptor_->EncryptString16(…);
PasswordStore 正常初始化时会注入 async encryptor,日常存密码没问题。
但云同步 merge 使用的是临时构造的 LoginDatabase,只调 Init(db_path),没有 inject encryptor。merge 最后一步:
cur_db.AddLoginEntrys(&new_entries, /*is_need_encrypt=*/true);
在 148 上每条 AddLogin 的 OSCrypt 加密全部失败,且旧实现里 AddLoginEntrys 仍可能 Commit() 成功——事务提交,但 0 行插入。
磁盘表现非常典型:
|
Login Data.tmp 约 40KB,logins 表 0 行 |
从空库 copy 来的 schema 壳,merge 没写进去 |
|
有 Sync Login Data.tmp-journal |
云端 tmp 被打开、读过 |
|
没有 Login Data.tmp-journal |
对 merge 目标 tmp 几乎没有有效写入 |
132 在同样路径下,EncryptedString 在 encryptor_ 为空时会 回退到 legacy OSCrypt::EncryptString16,merge 能写入 tmp。
修复思路:在定制分支恢复 132 的 OSCrypt 回退;并让 AddLoginEntrys 在「有 entries 但一条都没插入」时返回 false,避免 merge 误报成功。
3. Legacy os_crypt/sync 的 GN visibility 收紧
148 将 sync 版 OSCrypt 收成 component,并加了 visibility 白名单,默认只有 chrome/browser 等少数 target 能 deps。
merge 修复需要在 components/password_manager 里链接 //components/os_crypt/sync:sync(148 的 target 名;132 时代可能是 :os_crypt)。否则会:
- 链接阶段:OSCrypt::EncryptString16 undefined symbol
- gn gen 阶段:dependency not allowed / unresolved visibility
修复思路:
- 在 password_store 的 BUILD.gn 增加对 os_crypt/sync:sync 的 deps
- 在 os_crypt/sync 的 visibility 中放行 password_store 下的 target(可用 :* 表示该 BUILD 文件内全部 target)
依赖方向是 password_manager → os_crypt,visibility 只是「谁被允许依赖我」,不是反向依赖。
第三层:切库时的 SQLite sidecar(148 更容易踩)
即使 tmp 里已有数据,CopyAndSwitchDBFile 把 tmp 复制到正式 Login Data 时,若目标目录残留旧的 -journal / -wal / -shm,SQLite 打开时可能做 recovery,把刚复制的文件回滚成空库。
132 上有时因时序或 sidecar 状态不同,表现为「tmp 有数据、正式库空」。148 上更稳定复现。
修复思路:
- copy 前删除目标(及源)sidecar
- 检查 Init() 与 CheckLocalData(),不要只看 CopyFile 返回值
这一层与 OSCrypt 无关,属于 merge 之后的切库问题。
三层故障对照(一张表)
|
打开/读库 |
Init 失败、sqlite_schema |
sql 层 + SQLite 3.31.1 不匹配 |
SQL_CATALOG_TABLE → sqlite_master |
|
merge 写 tmp |
tmp 无 logins 行 |
async encryptor 必填 + AddLoginEntrys 误报 |
OSCrypt 回退 + 插入计数 |
|
tmp→正式库 |
tmp 有数据、Login Data 空 |
sidecar recovery |
CopyAndSwitchDBFile 清 sidecar |
三层缺一不可;只修 UI 或只修 handler 外层逻辑,无法根治。
为什么不是前端问题
密码管理页加载路径(132/148 一致):
passwords_section.ts
→ getCredentialGroups()
→ PasswordsPrivateDelegateImpl::GetCredentialGroups()
→ SavedPasswordsPresenter 内存缓存
缓存由 Init() / RefreshCaches() 通过 GetAllLoginsWithAffiliationAndBrandingInformation() 填充。
登录瞬间的 RefreshCaches 往往早于下载完成;但若 merge 已成功、正式库有数据,重新打开密码页仍应能看到条目——我们当时是 库本身为空,所以手动刷新无效。
排查建议(可复用的检查单)
看 tmp,不看 UI
merge 后直接查 Login Data.tmp 的 logins 表行数。0 行 → 优先查 OSCrypt / GetAllLoginsNoEncrypt。
看 sidecar
tmp 有 journal、正式库无数据 → 查 CopyAndSwitchDBFile 与 sidecar。
看 Init 日志
sqlite_schema → 查 sql catalog 宏与 SQLite 版本。
断点优先级
AddLoginEntrys → EncryptedString → GetAllLoginsNoEncrypt(云端)→ CopyAndSwitchDBFile → GetAllLogins(UI 路径)。
经验小结
大版本合并要区分「上游 intentional change」和「定制假设」
148 的 sqlite_schema、encryptor 必填、os_crypt visibility 都是上游演进;定制 SQLite 3.31.1 和 standalone merge DB 是旧假设,合并后会叠成多层故障。
对比磁盘状态比对比 UI 更快
「tmp 无行 / 正式库无行 / journal 在哪」直接指向 pipeline 的哪一段。
132 能跑不等于 148 该原样跑
132 的 OSCrypt 回退和 sqlite 侥幸路径掩盖了部分问题;148 更严格,反而把 bug 暴露清楚。
handler 与内核修复解耦
云同步 handler 的 merge 流程 132/148 一致时,不必为了 sync 去堆 handler 改动;应优先对齐 sql / login_database / BUILD 与 148 内核契约。
结语
这次问题的本质,不是「密码同步协议变了」,而是 Chromium 148 在 SQL catalog、OS 级加密抽象、构建可见性三处收紧,与「整库 merge + 旧版 SQLite + 无 encryptor 的临时 LoginDatabase」这一定制路径未对齐。
修复后,云同步仍走原有 blob 协议,无需改云端格式;关键是让 打开库 → 解密云端 → OSCrypt 写 tmp → 无 sidecar 切库 四步在 148 上重新成立。
若你也在维护 Chromium 大版本分叉,建议把 绑定 SQLite 版本 ↔ sql/ catalog API ↔ 密码 encryptor 注入点 列入合并 checklist,比事后查空表省得多。




