Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions docs/hermes-base-verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,22 @@
|---|---|
| `Bytecode File Information` 等头部 | 文本头不比较;raw 核对从二进制比较运行时选项、全局函数索引、segment ID、模块与函数源码表 |
| `Global String Table` | 不比较死字符串;有效 HBC 的字符串按二进制表恢复完整内容,而非截断或转义后的显示文本 |
| `Array Buffer` / `Object Key Buffer` / `Object Value Buffer`(v98:`Literal Value Buffer` / `Object Key Buffer`) | 文本段本身**不比较**(见下);无法读二进制缓冲区时整段文本仅用于诊断,不能据此判定等价 |
| `Array Buffer` / `Object Key Buffer` / `Object Value Buffer`(v98/v99:`Literal Value Buffer` / `Object Key Buffer`) | 文本段本身**不比较**(见下);无法读二进制缓冲区时整段文本仅用于诊断,不能据此判定等价 |
| `Function<…>` / `NCFunction<…>` / `Constructor<…>` 各函数 | 头文本必须相同;正文逐行经 `normalizeDisassemblyLine` 归一化后比较;**按函数分段**,错位不级联 |
| `Debug *` / `Textified callees table` | 排掉(只服务调试器/性能工具) |

**字面量按指令比较(`src/utils/hermes-literals.ts`)**:`NewArrayWithBuffer rX, sizeHint, count, offset` / `NewObjectWithBuffer rX, sizeHint, count, keyOffset, valueOffset` 的 offset 是序列化字面量缓冲区里的**字节偏移**。校验先从两份 HBC 文件读出头部和三段缓冲区(`hbcTransform.ts` 的布局表),在每条指令处按 SLP 格式解码 `count` 个条目(tag 字节:bit 6..4 类型、bit 7 长度续字节、bit 3..0 长度低 4 位;值按类型定宽:double 8 字节、字符串 id 4/2/1 字节、int32 4 字节),字符串 id 经各自字符串表解析成文本后写进归一化后的指令行,两侧逐条比较。

两种布局:

| | v87–96(`split`) | v98(`shaped`) |
| | v87–96(`split`) | v98/v99(`shaped`) |
|---|---|---|
| 缓冲区 | `arrayBuffer` / `objKeyBuffer` / `objValueBuffer` | `literalValueBuffer`(数组元素与对象值共用)/ `objKeyBuffer` / `objShapeTable`(8 字节项:keyOffset u32、numProps u32) |
| 数组 | `NewArrayWithBuffer rX, sizeHint, count, offset` | 同左,offset 指向值缓冲区 |
| 对象 | `NewObjectWithBuffer rX, sizeHint, count, keyOffset, valueOffset` | `NewObjectWithBuffer rX, shapeIndex, valueOffset`;`NewObjectWithBufferAndParent rX, rParent, shapeIndex, valueOffset` |
| tag 类型 6 | 1 字节字符串 id | `undefined`(无值字节);字符串 id 只用 2/4 字节 |

v98 的 shape 索引和 offset 一样只用于定位(delta 可能重排 shape 表),归一化后只留 shape 指向的键与值;`AndParent` 的父对象寄存器原样保留比较。
v98/v99 的 shape 索引和 offset 一样只用于定位(delta 可能重排 shape 表),归一化后只留 shape 指向的键与值;`AndParent` 的父对象寄存器原样保留比较。

为什么不能按 dump 的整段文本比:Hermes 的缓冲区构建器会**重叠/去重**序列化后的字面量——一个字面量的最后一个值字节可以同时是下一个字面量的 tag 字节(模糊测试实测:`61 52 | cd 09 b3 05 11`,前一段以 `[String 82]` 结尾,后一条指令的 offset 正指向 `52`)。顺序解析整段缓冲区(hermesc 的 dump 就是这么打印的)从这里开始失步,之后的条目全是噪声;delta 构建的 id 宽度不同,重叠位置也不同,于是两段"噪声"在某处不一致就被判为差异。2026-09-10 的 20 轮冒烟模糊测试里 3 次误杀全部源于此,改按指令比较后全部等价。无法读二进制缓冲区(文件结构不识别)时两侧一起比较整段文本,仅辅助诊断;即使文本相等也返回 `dump-failed` 并回退 plain,不能以丢失 offset/count 的文本确认等价。结果里 `literals: 'buffer'` 标明这一点。

Expand All @@ -43,14 +43,16 @@ v98 的 shape 索引和 offset 一样只用于定位(delta 可能重排 shape

**原始操作数核对**:`hermes-raw.ts` 从 raw dump 读取指令起点和操作数类型,并检查操作数与 HBC 字节一致、指令覆盖完整函数体。字符串从 small/overflow string table 与 string storage 按完整 ASCII/UTF-16 code unit 解码;BigInt、正则和 double 读取真实字节(保留 `-0` 和尾部精度);函数引用保留索引,与顺序对齐的函数表共同检查,同名函数不能互换。地址映射为目标指令序号;整数和字符串 switch 从二进制恢复 case 值与目的地。函数运行时 flags 和参数/寄存器等字段也参与比较,剔除的仅是物理地址、debug presence 与 compact/overflow 表示。

这不是一个可以忽略所有新指令的通用语义证明器。已验证的函数头范围为 HBC v87–96,以及 `hermes-compiler@250829098.0.16/.17` 的 v98 快照;升级布局或引入新的字符串/shape 引用指令时,需要复核 `hermes-raw.ts` 的解码规则及测试,不可仅扩展 diff-transform 的布局表。
这不是一个可以忽略所有新指令的通用语义证明器。已验证的函数头范围为 HBC v87–96、`hermes-compiler@250829098.0.16/.17` 的 v98 快照,以及 `hermes-compiler@260318099.0.4` 的 v99 快照;升级布局或引入新的字符串/shape 引用指令时,需要复核 `hermes-raw.ts` 的解码规则及测试,不可仅扩展 diff-transform 的布局表。

结果三态:`equivalent` / `different`(带第一处差异:函数、行号、两侧内容,或缓冲区条目)/ `dump-failed`(无法解析完整语义数据、dump 进程失败/超时/提前结束;带原因或 stderr 末行)。后两种都放弃 base,但日志分开。

**零长度函数**:Static Hermes 的真实 Metro 产物可能保留 `bytecodeSizeInBytes == 0` 的死函数。raw 校验要求实际指令字节总长等于函数头声明长度,而不是要求每个函数至少有一条指令;空函数仍比较运行时元数据,遗漏整个函数头仍由总函数数检查拒绝。非空函数的指令被截断仍为 `dump-failed`。

**v98 函数头快照边界**:当前大头按 37 字节、flags 位于 `[36]` 读取,小头 cache 位域为 6/1/1,绑定 `250829098` 稳定快照。上游 [7193d4485b](https://github.com/facebook/hermes/commit/7193d4485beeb87cd7a3b6ca8b6b5d97a1a433c4) 删除 `NumCacheNewObject` 后,仍报 v98 的构建曾使用 36 字节 / flags `[35]`、cache 位域 7/1。`hbcTransform` 的两套 v98 文件头布局不能识别这次**函数头**变化;本轮不宣称支持该后续快照,也不能仅凭文件头或版本号选择它。更换编译器时必须补对应的大头、小头和真实 Metro 测试;未审核的 v98 构建应使用 `--hermesBase none`。

**v99 函数头**:`hermes-compiler@260318099.0.4`(RN 0.88)是上述删除之后的首次版本号升级,文件头与段布局同晚期 v98(`hbcTransform` 共用一条布局),函数头一律是删除后的形态:大头 36 字节、flags 位于 `[35]`,小头 cache 位域 7/1。v99 新增的 typed 模式 `NewTypedObjectWithBuffer` 的字面量操作数尚未解码,遇到即 `dump-failed` 回退 plain。

pretty 输出本身会截断长字符串与 BigInt、用函数名替代函数索引,并可能把 `-0` 显示为 `0`,因此不再以 pretty 相等作为最终结论。新增归一化规则时必须同时添加真实 HBC 负例,确保没有把语义差异折叠掉。

## 3. 已完成与待办
Expand All @@ -70,7 +72,7 @@ SELECT hermesBaseOutcome, COUNT(*) FROM versions

旧版服务端忽略未知字段,CLI 先于服务端发布也安全。

### 3.2 指令级缓冲区偏移 —— 已完成(v87–96、v98)
### 3.2 指令级缓冲区偏移 —— 已完成(v87–96、v98、v99)

见 §2"字面量按指令比较"。v98 最初按 v96 的 tag 语义解码,模糊测试(300 轮,RN 0.87 的 hermes-compiler)报出 259 次对象值不一致——全部源于 tag 类型 6 在 v98 表示 `undefined` 而非 1 字节字符串 id;修正后 v98 两个种子共 800 轮:0 次差异、75 处植入差异全部抓到。

Expand All @@ -90,9 +92,10 @@ raw 核对保留函数引用索引,与出现顺序对齐的函数体及二进
- **资源开销**:base/plain 编译并发完成后,执行 pretty 和 raw 两遍 dump;每遍两个进程,raw 不增加编译。二进制元数据读取目前持有两份 HBC、完整字符串映射及字面量缓冲区,反汇编仅保留当前函数;这是完整数据核对的额外内存和时间开销。不要以删除验证数据来优化内存,可后续改为按段读取或降低并行度。
- **进程期限**:版本探测默认 30 秒(`PUSHY_HERMES_PROBE_TIMEOUT_MS`),完整校验两遍合计默认 120 秒(`PUSHY_HERMES_VERIFY_TIMEOUT_MS`),单个编译/源码映射子进程默认 300 秒(`PUSHY_HERMES_COMPILE_TIMEOUT_MS`)。环境变量单位均为毫秒,必须是 1–2147483647 的整数,否则用默认值。超时终止子进程,优化失败回退 plain;真正的 plain 编译或最终 sourcemap 失败仍使构建失败。校验函数还接受 `AbortSignal`;base 下载任务的取消传播尚未统一。
- **源码映射竞态**:推测执行的 base sourcemap 合成任务启动时立即观察拒绝,之后再根据最终采用哪份字节码决定抛出错误还是为 plain 重做合成。
- **CI**:`hermes-hbc-96` / `hermes-hbc-98` / `hermes-hbc-98-patch17` 分别安装固定的 `react-native@0.77.3` / `hermes-compiler@250829098.0.16` / `.17`。除小程序回归与 50 轮固定种子 fuzz 外,三组都执行真实 Metro bundle 的自比和 base/plain 比较;v98 测试断言产物确实包含零长度函数。缺编译器或指定的 fixture 缺失/哈希不符会失败,不静默跳过。
- **CI**:`hermes-hbc-96` / `hermes-hbc-98` / `hermes-hbc-98-patch17` / `hermes-hbc-99` 分别安装固定的 `react-native@0.77.3` / `hermes-compiler@250829098.0.16` / `.17` / `hermes-compiler@260318099.0.4`。除小程序回归与 50 轮固定种子 fuzz 外,四组都执行真实 Metro bundle 的自比和 base/plain 比较;v98 测试断言产物确实包含零长度函数。缺编译器或指定的 fixture 缺失/哈希不符会失败,不静默跳过。
- **真实 Metro 数据**:[hbc-diff-benchmark](https://github.com/sunnylqm/hbc-diff-benchmark/tree/e6a870a1c26c4b64c7860d7e1aa575707d22ad88) 的 `base.jsbundle` 和 `s3-medium-feature.jsbundle`(MIT,保留其 LICENSE;来源与生成步骤见该仓库 `fixtures/GENERATION.md`)。CI 固定提交和 SHA256,不运行下载的 JS,仅交给固定编译器。离线运行:`HERMESC=<hermesc> HERMES_METRO_FIXTURES=<fixtures目录> bun test tests/hermes-metro.test.ts`;普通单元测试不联网下载。
- **调试输出异常回归**:`tests/hermes-blockers.test.ts` 在独立进程覆盖带 `dumpTo` 的超时、运行中取消、预取消、ENOENT,同时检查返回标记、退出状态和晚到的异步错误。错误处理立即 unpipe 全部目的地并结束 debug 文件,避免 `finish()` 等待一个只会在后续 `kill()` 中结束的流;Node 18 CI 也执行相同场景。
- **不要删掉 pretty 这一遍**:raw 指令归一化不覆盖异常处理表等信息,pretty 对比仍是必要的一层(PR #85 评审结论)。
- **`hbcdump`/`hbc-diff`**:Hermes 仓库自带的工具,RN 的 hermesc 不随附;如果将来 hermes-compiler 包里带上,可替代文本 dump 解析。

## 4. 明确接受的剩余风险
Expand All @@ -104,6 +107,6 @@ raw 核对保留函数引用索引,与出现顺序对齐的函数体及二进
1. 看日志里的 detail:函数 + 行号 + 两侧内容,或缓冲区条目,或 dump 失败原因。
2. 两行只差宽度后缀/数字 id/偏移:先核对解析后的完整值与控制流目标。只有证明是表示差异后才能补归一化规则,不能直接删除操作数;同时添加负例与 fuzz。
3. 指令数量/顺序/opcode 不同,或某条 `New*WithBuffer` 解析后的字面量不同(detail 形如 `NewArrayWithBuffer r2 size=44 n=44 entry 7: [String "a"] vs [String "b"]`)→ 真差异,回退正确,向 Hermes 上游报告。detail 带 `undecodable` 说明无法解码,归为 `dump-failed`;可能是损坏的 HBC,也可能是验证器尚不支持的布局,不能直接断言上游错误。
若日志/元数据里 `literals` 为 `buffer`(文件结构不识别),缓冲区是按整段文本比的,`Array Buffer entry N` 形式的差异可能是 §2 的重叠误杀,需人工用二进制解码核对。
detail 形如 `Array Buffer entry N: … vs …` 时,说明文件结构未被识别、缓冲区是按整段 dump 文本比的(比较结果的 `literals` 为 `buffer`,该字段不进日志),差异可能是 §2 的重叠误杀,需人工用二进制解码核对。
4. `dump-failed` → 校验无法完成;检查解析原因、编译器版本、超时与 stderr。它不是等价证据,也不必然是与字节码无关的进程错误。
5. 需要完整现场:`PUSHY_HERMES_BASE_DEBUG=1 pushy bundle …`,两份反汇编在中间目录旁 `hermes-base-dump-{base,plain}.txt`。
52 changes: 0 additions & 52 deletions docs/hermes-review-follow-up.md

This file was deleted.

1 change: 0 additions & 1 deletion src/bundle-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1358,7 +1358,6 @@ async function selectHermesBase(
try {
const base = await resolveHermesBase({
option: request.option,
hermesCommand,
bytecodeVersion,
appId: request.appId,
cacheMaxMb: request.cacheMaxMb,
Expand Down
Loading
Loading