Skip to content

Commit 4704131

Browse files
sunnylqmclaude
andcommitted
feat(hermes): support HBC v99 (hermes-compiler 260318099, RN 0.88)
HBC 99 keeps the late v98 file header and section layout, so the diff transform reuses it. The semantic audit behind --verifyHermesBase now reads the v99 function headers (NumCacheNewObject removed: 36-byte large header, 7-bit write cache in the small header) and fails closed on the typed-mode NewTypedObjectWithBuffer. CI runs the real-compiler suite against hermes-compiler 260318099.0.4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 7e819c2 commit 4704131

5 files changed

Lines changed: 85 additions & 32 deletions

File tree

‎.github/workflows/test.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@ jobs:
169169
package: hermes-compiler@250829098.0.17
170170
directory: hermes-compiler
171171
executable: hermesc/linux64-bin/hermesc
172+
- hbc: 99
173+
suffix: ""
174+
package: hermes-compiler@260318099.0.4
175+
directory: hermes-compiler
176+
executable: hermesc/linux64-bin/hermesc
172177
steps:
173178
- uses: actions/checkout@v7
174179
with:

‎src/utils/hbcTransform.ts‎

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,31 @@ const FUNC_OFFSET_25: DeltaField = { byte: 0, bit: 0, bits: 25 };
4545
const STRING_OFFSET_23: DeltaField = { byte: 0, bit: 1, bits: 23 };
4646
const PAIR_OFFSET_32: DeltaField = { byte: 0, bit: 0, bits: 32 };
4747

48-
// v98 两种头部变体共享的段布局
48+
// 晚期 v98 与 v99 共用的头部计数字段
49+
const V98_LATE_HEADER_FIELDS = [
50+
'fileLength',
51+
'globalCodeIndex',
52+
'functionCount',
53+
'stringKindCount',
54+
'identifierCount',
55+
'stringCount',
56+
'overflowStringCount',
57+
'stringStorageSize',
58+
'bigIntCount',
59+
'bigIntStorageSize',
60+
'regExpCount',
61+
'regExpStorageSize',
62+
'literalValueBufferSize',
63+
'objKeyBufferSize',
64+
'objShapeTableCount',
65+
'numStringSwitchImms',
66+
'segmentID',
67+
'cjsModuleCount',
68+
'functionSourceCount',
69+
'debugInfoOffset',
70+
];
71+
72+
// v98 两种头部变体(及 v99)共享的段布局
4973
const V98_SECTIONS: SectionDesc[] = [
5074
{
5175
name: 'functionHeaders',
@@ -195,28 +219,16 @@ export const HBC_LAYOUTS: HbcLayout[] = [
195219
// 段布局与早期 v98 相同。
196220
minVersion: 98,
197221
maxVersion: 98,
198-
headerFields: [
199-
'fileLength',
200-
'globalCodeIndex',
201-
'functionCount',
202-
'stringKindCount',
203-
'identifierCount',
204-
'stringCount',
205-
'overflowStringCount',
206-
'stringStorageSize',
207-
'bigIntCount',
208-
'bigIntStorageSize',
209-
'regExpCount',
210-
'regExpStorageSize',
211-
'literalValueBufferSize',
212-
'objKeyBufferSize',
213-
'objShapeTableCount',
214-
'numStringSwitchImms',
215-
'segmentID',
216-
'cjsModuleCount',
217-
'functionSourceCount',
218-
'debugInfoOffset',
219-
],
222+
headerFields: V98_LATE_HEADER_FIELDS,
223+
sections: V98_SECTIONS,
224+
},
225+
{
226+
// Hermes V1 v99(hermes-compiler 260318099,RN 0.88+):文件头与段布局
227+
// 同晚期 v98,只改了函数头内部位域(去 NumCacheNewObject)与操作码表,
228+
// 二者都不在本变换触及的范围内。
229+
minVersion: 99,
230+
maxVersion: 99,
231+
headerFields: V98_LATE_HEADER_FIELDS,
220232
sections: V98_SECTIONS,
221233
},
222234
{

‎src/utils/hermes-raw.ts‎

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ export async function readHermesSemanticData(
6868
if (
6969
!(
7070
(resolved.version >= 87 && resolved.version <= 96) ||
71-
resolved.version === 98
71+
resolved.version === 98 ||
72+
resolved.version === 99
7273
)
7374
) {
7475
throw new UnverifiableHermesBytecode('unsupported semantic HBC version');
@@ -105,7 +106,11 @@ export async function readHermesSemanticData(
105106
// hbcTransform's file-header variants do not distinguish that function
106107
// schema change. A compiler upgrade needs independent large/small fixtures;
107108
// do not infer either schema from numStringSwitchImms or HBC version alone.
108-
const shaped = resolved.version === 98;
109+
// Audited v99 function schema: hermes-compiler 260318099.0.4 (RN 0.88), the
110+
// first HBC bump after that removal, so it always has the 36-byte form.
111+
const shaped = resolved.version >= 98;
112+
const noCacheNewObject = resolved.version === 99;
113+
const largeSize = noCacheNewObject ? 36 : 37;
109114
const entrySize = shaped ? 12 : 16;
110115
const headers = section('functionHeaders');
111116
const functions: FunctionData[] = [];
@@ -122,15 +127,17 @@ export async function readHermesSemanticData(
122127
const largeOffset = shaped
123128
? ((word1 >>> 14) & 0xff) * 0x1000000 + offset
124129
: (headers.readUInt32LE(at + 8) & 0x1ffffff) * 0x10000 + offset;
125-
const large = checkedSlice(bytes, largeOffset, shaped ? 37 : 31);
130+
const large = checkedSlice(bytes, largeOffset, shaped ? largeSize : 31);
126131
offset = large.readUInt32LE(0);
127132
size = large.readUInt32LE(shaped ? 12 : 8);
128133
name = large.readUInt32LE(shaped ? 16 : 12);
129-
flags = large[shaped ? 36 : 30];
134+
flags = large[shaped ? largeSize - 1 : 30];
130135
fields = shaped
131136
? [4, 8, 20, 24, 28].map((p) => large.readUInt32LE(p))
132137
: [4, 20, 24].map((p) => large.readUInt32LE(p));
133-
fields.push(...large.subarray(shaped ? 32 : 28, shaped ? 36 : 30));
138+
fields.push(
139+
...large.subarray(shaped ? 32 : 28, shaped ? largeSize - 1 : 30),
140+
);
134141
} else if (shaped) {
135142
size = word1 & 0x3fff;
136143
name = (word1 >>> 14) & 0xff;
@@ -141,9 +148,13 @@ export async function readHermesSemanticData(
141148
word1 >>> 27,
142149
headers[at + 8],
143150
headers[at + 9],
144-
headers[at + 10] & 63,
145-
(headers[at + 10] >>> 6) & 1,
146-
headers[at + 10] >>> 7,
151+
...(noCacheNewObject
152+
? [headers[at + 10] & 127, headers[at + 10] >>> 7]
153+
: [
154+
headers[at + 10] & 63,
155+
(headers[at + 10] >>> 6) & 1,
156+
headers[at + 10] >>> 7,
157+
]),
147158
];
148159
} else {
149160
size = word1 & 0x7fff;
@@ -435,6 +446,9 @@ export function normalizeRawHermesFunction(
435446
'undecodable shape values',
436447
);
437448
}
449+
} else if (op === 'NewTypedObjectWithBuffer') {
450+
// HBC v99 typed-mode buffer user; its literal operands are not decoded.
451+
throw new UnverifiableHermesBytecode('typed object literal buffer');
438452
} else if (op === 'CacheNewObject') {
439453
if (buffers.layout !== 'shaped' || values.length !== 4) {
440454
throw new UnverifiableHermesBytecode('unknown cached object operands');

‎tests/hbc-transform.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,34 @@ function buildSyntheticV98(
114114
return buf;
115115
}
116116

117+
/** v99 的文件头与段布局同晚期 v98,只有版本号不同。 */
118+
function buildSyntheticV99(): Buffer {
119+
const buf = buildSyntheticV98('late');
120+
buf.writeUInt32LE(99, 8);
121+
return buf;
122+
}
123+
117124
describe('hbcTransform', () => {
118125
test('recognizes fixture version and layout', () => {
119126
const a = fixture('v96-a.hbc');
120127
expect(getHbcVersion(a)).toBe(96);
121128
expect(findLayouts(96)).toHaveLength(1);
122129
expect(findLayouts(97)).toHaveLength(0);
123130
expect(findLayouts(98)).toHaveLength(2); // 晚期(20 槽)优先,早期(19 槽)兜底
131+
expect(findLayouts(99)).toHaveLength(1);
132+
expect(findLayouts(99)[0]!.headerFields).toEqual(
133+
findLayouts(98)[0]!.headerFields,
134+
);
135+
expect(findLayouts(100)).toHaveLength(0);
136+
});
137+
138+
test('v99 is transformed with the late v98 file layout', () => {
139+
const buf = buildSyntheticV99();
140+
expect(getHbcVersion(buf)).toBe(99);
141+
const t = transformHbc(buf);
142+
expect(t).not.toBeNull();
143+
expect(Buffer.compare(t!, buf)).not.toBe(0);
144+
expect(Buffer.compare(transformHbc(t!, true)!, buf)).toBe(0);
124145
});
125146

126147
test('transform is invertible on real v96 fixtures', () => {
@@ -245,6 +266,7 @@ describe('hbcTransform', () => {
245266
[fixture('v96-b.hbc'), layout96],
246267
[buildSyntheticV98('late'), findLayouts(98)[0]!],
247268
[buildSyntheticV98('early'), findLayouts(98)[1]!],
269+
[buildSyntheticV99(), findLayouts(99)[0]!],
248270
];
249271
for (const [buf, layout] of cases) {
250272
const expected = transformHbcWithLayout(buf, layout, false)!;

‎tests/hermes-raw.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ describe.if(hasHermesc)('lossless Hermes operand audit (real compiler)', () => {
129129
);
130130
});
131131

132-
test.skipIf(!hasHermesc || probeHbcVersion(hermesc!) === 98)(
132+
test.skipIf(!hasHermesc || probeHbcVersion(hermesc!) >= 98)(
133133
'classic global lexical declarations resolve restricted-property string IDs',
134134
async () => {
135135
const base = compile('lexical-base', 'print("old-base-string");');

0 commit comments

Comments
 (0)