Repository navigation
这一版你会看到什么
- 启动哈希默认「自动」:重装/新装的机器一个字都不用填,模块从当前后端自己的配置里取
verifiedBootHash写进ro.boot.vbmeta.digest;取不到(比如vb_hash=random)就一个字节都不写。
升级前手填过值的机器行为完全不变(那一档仍是手动)。 - 属性伪装补到 43 个属性名:
userdebug/test-keys在每个分区的*.build.*与指纹里
就地改掉,不再出现"顶层说 user、分区说 userdebug"的自相矛盾;本机没有的分区报「本机无」,
不凭空写。 - 属性页修掉一处画错的颜色:本机没有的分区原来被画成红色「不符」,现在走中性。
- 包小了一半:533KB(上版 729KB),二进制 326KB(上版 653KB)——启动路径与界面行为不变。
安装:从管理器刷入 zip 即可;已装旧版的直接覆盖。
变更
-
打包不再把运行时缓存带出去:
tools/build-zip.mjs的 walk 会把module/webroot/icons/
(设备上webui.sh icons写的应用图标缓存,.gitignore里本来就是运行期目录)打进发布包 ——
v3.0.2 实测带了 3 张、33KB。现在 walk 跳过webroot/icons与bin(后者由脚本显式追加,
磁盘上有残留就会变成两条同名条目),并在--check里加两条断言:
「包里没有webroot/icons/」「bin/keylens只有一条」;落盘前也自证一次,跳过表失效时当场退出。
第一步 -20,924 字节(728,684 → 707,760),条目 34 → 31;加上后面的二进制优化,
最终 728,684 → 533,142(-26.8%)。 -
交付体积的构成(优化前量的):压缩后
keylens368KB(52%)、index.html241KB(34%)、
shell 层 91KB(13%)、其余 3KB —— 大头是二进制,不是界面,所以下一刀落在二进制上。 -
二进制 -50%(652,960 → 326,416 字节):新增
rust/build-release.sh固定发布构建 ——
nightly +-Z build-std=std,panic_abort+-Zunstable-options -Cpanic=immediate-abort
(std 跟着opt-level=z重编、panic 的格式化与展开整块去掉)。CI 改成装 nightly +
rust-src + 两个 musl target 后调这个脚本。发布包因此 728,684 → 533,142 字节(-26.8%)。
代价与前提写进脚本抬头:panic 时不打印消息、退出码由 101 变 SIGABRT;命令的错误路径
全走 Result + 自己的退出码,247 项对拍、cargo test72、真机device-boot-hash20 条与
device-props-mask --write17 条(跑完现场还原)都在这份产物上验过。 -
量过但决定不做的三刀(都留了数):去 unwind 表(产物一字未变,
.eh_frame来自预编译
std/crt)、tokens 瘦身(154/382 个变量没人用,只值 8.8KB)、color-spaces 换小实现
(133KB 覆盖 20 个色彩空间且被泛型遍历,收益约 40KB 压缩而风险是色值静默偏移)。 -
WebUI 台架加性能基线(
verify.mjs末尾,3 次导航取中位):首屏可见 45ms · FCP 48ms ·
脚本 15ms · 布局 3ms(本机 Chrome 417×975@2x)。第一版量测写错了并当场被自检抓住:
Performance.getMetrics跨文档导航会清零,做差得负值;改成导航完读一次累积值。 -
「启动哈希」改成三态,默认是「自动」——新用户一个字都不用填。以前这一格只有
「填了 / 没填」两态,而多数 ROM(本机是 LineageOS)根本不给ro.boot.vbmeta.digest,
于是"钥匙盒在证明一个启动哈希、系统属性里那格却是空的"正好踩中检测器的
vbmetaDigestMissingWhileAttestedHashPresent(这台机实测 Danger 4 → 2 的差就是它)。
现在:文件不存在 = auto,开机时从当前后端自己的配置取(OMK 的[trust] vb_hash、
TEESim 的overrides.json verifiedBootHash),拿到合法 hex 才写;文件里是 hex =
manual(优先,升级前填过的机器行为不变);文件里是off= 明确不写(旧版
「清除」的语义搬到这里)。界面那一格跟着改成三态,并给出「保存并应用 / 改为自动 /
不写 digest」三个动作。
为什么 random 必须熄火:OMK 允许vb_hash = "random"(每次 keymint 启动重新生成),
镜像过来的值下一开机就过期,而检测器对"过期值"和"空值"给的是同一级 Danger——
写了反而更糟。所以派生只认 64/128 位十六进制,random/null/缺文件/注释行一律当
取不到,且绝不缓存:每次开机、每次应用都重读后端配置。只问启用的那一家后端,
不读卸载后残留的配置目录(那会把一个不再被证明的 hash 写进 prop)。
真机正对照:夹具里只放真实的config.toml、不放 boot_hash 文件,auto 算出的值与
手抄那串0561fff0…逐字节相同。 -
属性伪装补齐「构建身份」这一族:期望表从 17 项涨到 43 个属性名。以前只钉顶层
ro.build.type/ro.build.tags,而每个分区还有一套自己的
*.build.type(本机现量 8 个还是userdebug)、*.build.tags、*.build.fingerprint
(ro.system.build.fingerprint尾巴就写着userdebug),再加上ro.build.flavor
(lineage_pdx234-userdebug)。结果是「顶层说 user、分区说 userdebug」这种自相矛盾——
检测器今天只把它印在[Info]不判级,但任何应用逐键都读得到,而矛盾比统一的
userdebug 更难看。特征表从 2 行补到 38 行。 -
特征行从「整值替换」改成「就地替换每一处命中」。旧语义把
ro.bootloader=engineering-x直接压成release、把整条ro.build.description
压成release-keys—— 那比不改更像被动过。同名多条规则(description、每个指纹)
现在链式串完只落一次盘。 -
本机没有的分区报
absent:不计入「写入并复核」的不符项,也绝不凭空写出来。
凭空多一个ro.system_dlkm.build.type本身就是破绽——stock 不会给不存在的分区留属性。
固定表那 15 项保留「缺失就补」的原语义,没动。 -
判定改成一次
getprop+ 一次 awk 出计划,这是上面两条能落地的前提。
两条看着最自然的方案都被真机读数否掉了:逐键读 43 行 = 1484ms;
把 dump 读进 shell 变量再查表更糟——mksh 的字面匹配随字符串长度平方级增长
(66897 字节上 15 次匹配实测 160307ms,先grep -F裁到 2.7KB 后 53 次仍要 1841ms)。
还有一堵是 设备上printf不是内置:在 shell 里逐行拼数据,53 行 ≈ 850ms,
比 awk 本体贵一个数量级。现在表原样交给 awk,shell 只执行计划:
props_plan942ms→208ms,同一张表 awk 路 485ms vs 逐键回落 1484ms。
修复
- 属性页会把「本机没有这个分区」画成红色「不符」。界面那句状态分派只有
ok / missing / 其余算不符,命令层新增的第四种状态absent掉进 else:行上标红、
标题写「13 项不符」,而命令层同一刻报的是mismatch=0。现在 absent 走中性
(灰点 + 「本机无」+ 一句"这台机没有这项属性,模块不写它"),标题只数
diff/missing。
为什么对拍 216 项、真机 15 条都没抓到:缺陷不在命令层,而在"这一屏从没按真形状
画过一次"——台架的props假数据是手写 5 行,其中ro.product.brand、ro.serialno
根本不在期望表里。已把假数据换成真机现量的 53 行(vendor_dlkm 那一族 4 行除外,
这台机有那个分区、给不出 absent,只能在台架里补),并给行数/配色/标题三条断言
各配一个变异。 device-ab --write跑完没把运行期属性还原。它原来只快照伪装开关与 boot hash,
表一补齐,props-apply就会把分区属性掩上并留在那里——是它自己那条「差项数与跑前相同」
的不变量报出来的(跑前 10、跑后 0)。现在逐个键快照 + 还原 + 逐字节比对。
这是 2026-11-01 那次「台子把主人设备弄坏」的同类项,只是这回闸先响了。- 两侧读法对齐:Rust 的复核不再拿
resetprop给失效的getprop兜底(留着就会出现
「shell 读不到、二进制读得到」的假差异),对拍的桩也统一成一份存储
(以前两个桩各拿一份来源,改完判定层后两侧决策不同,mismatch差 16 项)。
闸
- 新增
tests/props-plan.sh(30 条):表形状、四列与状态、坑值(值里有]、以[开头、
带空格、中文、空值)、链式改只落一次盘、absent 不进计划、awk 路与逐键回落路落到同一个
属性区;三处变异各自自证。 - 新增
tests/device-props-mask.sh --write(真机 17 条,按现跑的 ok 行数):台子自己种出待改状态再测
(不能指望现场正好在泄漏,否则"改对了"这条正对照其实是"没测到"),臂 A 实测改写
11 个属性、shell 与二进制两臂从同一起点跑完 43 个键逐字节相同、属性区无新增键、
applied=1 / mismatch=0、
toybox awk 与 busybox awk 结果逐字节相同(本轮在 mawk 上撞到一个方言差:
赋值语句里的(k in goal)会被 mawk 提前创建元素,链式改写整条变成写空值,gawk 却不这样)、
跑完还原到原始现场。不加--write直接 skip,不在 CI 上改现场。 - 对拍 205 → 216 项:两侧写完的属性区逐字节相同(状态 A/B,不只比 stdout)、
链式改只落一次盘、absent 不凭空创建、getprop全废的降级三条。 - 新增
tests/device-boot-hash.sh(真机 20 条,全程只读):夹具里只放真实
config.toml、不放 boot_hash 文件,断言 auto 算出的 effective 与参照值逐字节相同
(现场是手动档取手抄那串;默认的 auto 档没有文件,取已在 prop 里生效的那串 —— 台子按
档位选,不把"没测"报成失败);另有 4 条钉 TEESim 那一支(夹具里放诱饵 override,
必须采用属性里那串而不是诱饵);random/off/invalid/无后端四种形状各自熄火;
跑完核对现场档位没变。 tests/props-boot-hash.sh:20 → 52 条(上一版文档写的「22 条」早就过期了,本轮按
现跑的 ok 行数改口):三态 + 派生 + random/注释行/不带引号/不缓存 + 变异(摘掉「只认 hex」
的守卫 ⇒random被当成值)。对拍 216 → 239 项(启动哈希三态 23 条)。
mutate-verify.mjs从 5 个变异加到 6 个(第 ⑥ 个把界面 auto 档说回「未设置」,必须响)。- 修掉一处读数口径分叉(真机现场切 auto 时抓出来的):
props-boot-hash的current=
原先两侧都用resetprop读,而生产的 shell 侧prop_now(module/webui.sh)用的是
getprop。裸 shell 里resetprop不在 PATH,命令于是报current=(空),属性其实有值。
对拍当时抓不到,因为夹具pb.sh/pba.sh自己手抄了一份 resetprop 版prop_now——
台子和被测物共用同一把坏仪器。现在夹具改成从module/webui.sh抽那一份,current=两侧
统一走getprop(写完的复核仍走resetprop,两边同),并补RP_READ_DEAD=1这条控制与
「那种情况下current=必须等于真值」的绝对判据:只加"两侧相同"不够,那版两侧一起报空照样过。
两个变异各自自证(夹具退回手抄版 → 两侧相同那条响;Rust 退回 resetprop → 绝对判据那条响)。
对拍 239 → 242 项(下限同步抬)。 - TEESim 那一侧的 auto 不再覆盖守护进程(读上游源码查出,
docs/HANDOFF.md§5.15 记了行号):
TEESim 每次 push 都把ro.boot.vbmeta.digest强制成它自己正在证明的值
(App.kt:510-523「the value we attest wins unconditionally」,含 bootloader 根本不填这格的
机器,上游 #228),而overrides.json里那串 hex 只有当它当前是合成兜底值时才被采纳
(Harvester.kt:183-189+applyUserOverrides:399-415对不可编辑字段直接 continue)。
所以 auto 在 teesim 下改成:属性已有合法值 → 采用它(source=daemon,一个字都不写);
属性为空 → 才按 override 补(老版本 TEESim 没有那条强制)。拿 override 去覆盖已有值,
恰好会在上游不采纳时亲手造出vbmetaDigestMismatch(与空值同级的 Danger)。
OMK 那一家相反:vb_hash是显式 hex 时上游不写这条 prop(vbmeta.rs:452-462的
needs_sysprop_write不含 ExplicitHex/Property),所以 omk 分支保持"照配置写、可覆盖"。
对拍 242 → 247 项(新增 5 条:属性空时两侧逐字节 + 取 override、计数器自证「该写时数得到 1」、
属性已有值时两侧逐字节 +daemon/那串+ 写次数 0 + 跑完 store 仍是守护进程那串);
单元 52 → 64 条(新增source=daemon一支与变异「摘掉那一行 ⇒ override 覆盖」);
界面多认一个来源(?bh=daemon画出来是「TEE Simulator 正在证明的值(属性里已有,模块不覆盖)」),
verify.mjs加 1 条断言、mutate-verify.mjs6 → 7 个变异(⑦ 把 daemon 说回 override;
实测 7 个全响,还原后重新构建复跑退码 0)。 - 顺带查清并写下:TEESim 的多 profile 与这一格无关 —— 一份 profile 装的是设备身份 /
keybox / mode / 三档补丁级别 /apps[]/autoIncludeNewApps(ConfigStore.kt:23-46),
里面没有 root of trust;证明里的 verifiedBootHash 在 profile 循环之外一次性写进
bootInfo(Resolver.kt:41-43),而 overrides.json 上游明写是扁平一张表
(OverrideStore.kt:6-9)⇒ 切 profile 不会让 digest 过期,这一格不需要"选哪一份 profile"。
模块的多 profile 支持本来就有:teesim-profiles列全部(名字/keybox/mode/apps/是否默认)、
teesim-profile-new复制 default、teesim-profile-del删(default 是兜底不许删)、
写路径按 payload 里的profile=<名字>决定"改哪一份"(省略仍是 default,旧界面行为不变),
界面先选份再编辑。 - 新增
tools/wsl-parity.sh:在 WSL 侧跑对拍并把结论和自己的退码写在同一行
(parity exit=0|全部通过(chk 用例 242 项))。理由是本坑:从 Git Bash 内联
wsl bash -c '…; echo $?'里的$?会被本地 shell 提前展开,量到的永远是 0 —— 本轮
差点据此误报"对拍的退码逻辑坏了、CI 那道闸形同虚设"。这条 runner 自带反面对照
(指一个不存在的二进制 →parity exit=1),也顺带被对拍的陈旧守卫救了一次。 - 台子覆盖:真机 A/B 38 条全过,shell 测试套 53 套在 WSL(mawk) 与 Git Bash(gawk) 两边全绿。
- 界面这一层:
tools/md3e-build/verify.mjs加 4 条属性页断言(53 行逐行上屏、absent 画
「本机无」、标题只数 9 项、absent 不挂 warn 色),mutate-verify.mjs从 2 个变异加到
5 个且全部会响;尾部截图new-props-tail.png进台架产物。