From bc9a02d3e9daef9296930ee4e7351395f79565ab Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Thu, 24 Sep 2026 08:21:15 +0800 Subject: [PATCH 1/3] test(hermes-base): real-compiler negative for the folded switch offset The pretty pass folds the (UInt)SwitchImm jump-table offset because a foreign base shifts it; v96 prints the classic `SwitchImm` name, which v2.26.1 missed and rejected good bases for (11 production records, 2026-09-11..14). The fold only had positive real-compiler coverage. Compile a dense switch against a foreign base, assert the offset really moved and the pair is equivalent, then swap two jump-table targets in the delta build and assert it is rejected. Passes on HBC v96 and v98. Co-Authored-By: Claude Opus 5.5 --- tests/hermes-raw.test.ts | 44 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/hermes-raw.test.ts b/tests/hermes-raw.test.ts index 9245250..6a29cd2 100644 --- a/tests/hermes-raw.test.ts +++ b/tests/hermes-raw.test.ts @@ -236,6 +236,50 @@ describe.if(hasHermesc)('lossless Hermes operand audit (real compiler)', () => { ); }); + // The pretty pass folds the jump-table offset of (UInt)SwitchImm because a + // foreign base shifts it (v96 prints the classic `SwitchImm` name, which + // v2.26.1 missed and rejected good bases for). Folding the offset must not + // fold what the table holds: swapping two case targets in the delta build + // keeps every instruction byte and still has to be rejected. + test('a folded switch offset still compares the jump-table targets', async () => { + const base = compile( + 'switch-base', + `globalThis.strings = ${JSON.stringify(Array.from({ length: 400 }, (_, i) => `foreign${i}`))};`, + ); + const cases = Array.from( + { length: 32 }, + (_, i) => `case ${i}: return o.k${i} + "v${i}";`, + ).join('\n'); + const source = `globalThis.oi = function oi(x, o, a){ var t = o.alpha + a.beta; switch(x){${cases} default: return t;}};`; + const plain = compile('switch-plain', source); + const delta = compile('switch-delta', source, base); + const isSwitch = (op: string) => + op === 'SwitchImm' || op === 'UIntSwitchImm'; + const [plainSwitch] = (await operands(plain, isSwitch)).found; + const { data, found } = await operands(delta, isSwitch); + expect(found).toHaveLength(1); + const [inst] = found; + // the offset really moved, so the fold is what makes these equivalent + expect(inst.values[1]).not.toBe(plainSwitch.values[1]); + const unchanged = await compareHermesBytecode(hermesc!, delta, plain); + expect(unchanged.status, unchanged.detail).toBe('equivalent'); + + // table entries are Int32 targets relative to the instruction, 4-byte aligned + const start = Math.ceil((inst.positions[0] - 1 + inst.values[1]) / 4) * 4; + const bytes = Buffer.from(data.bytes); + const first = bytes.readInt32LE(start); + const sixth = bytes.readInt32LE(start + 20); + expect(first).not.toBe(sixth); + bytes.writeInt32LE(sixth, start); + bytes.writeInt32LE(first, start + 20); + const result = await compareHermesBytecode( + hermesc!, + rewrite(delta, bytes), + plain, + ); + expect(result.status).toBe('different'); + }); + // hermesc annotates the string operand of DefineOwnByIdLong but not of // DefineOwnById, so the same instruction prints the text in one build and a // bare id in the other -- and pretty output cuts that text to a display From 5f3ac3c23d4ca49284884ea6f438091464de4a29 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Thu, 24 Sep 2026 00:56:20 +0000 Subject: [PATCH 2/3] test(hermes-base): assert raw instruction detail for swapped switch targets --- tests/hermes-raw.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/hermes-raw.test.ts b/tests/hermes-raw.test.ts index 6a29cd2..5891a4d 100644 --- a/tests/hermes-raw.test.ts +++ b/tests/hermes-raw.test.ts @@ -278,6 +278,7 @@ describe.if(hasHermesc)('lossless Hermes operand audit (real compiler)', () => { plain, ); expect(result.status).toBe('different'); + expect(result.detail).toContain('raw instruction'); }); // hermesc annotates the string operand of DefineOwnByIdLong but not of From bdc1d926cba11b966717ebd3089b9f9f89011ede Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Thu, 24 Sep 2026 09:03:47 +0800 Subject: [PATCH 3/3] test(hermes-base): check the raw audit rejects swapped switch targets on its own The pretty pass catches the swap first by the table's label order, so the full compare never reaches the raw audit and its detail is the pretty diff. Keep the compare assertion and run auditRawHermesBytecode directly for the raw-instruction check. Co-Authored-By: Claude Opus 5.5 --- tests/hermes-raw.test.ts | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/tests/hermes-raw.test.ts b/tests/hermes-raw.test.ts index 5891a4d..38b1489 100644 --- a/tests/hermes-raw.test.ts +++ b/tests/hermes-raw.test.ts @@ -8,7 +8,11 @@ import { compareHermesBytecode, probeHbcVersion, } from '../src/utils/hermes-base'; -import { readHermesSemanticData } from '../src/utils/hermes-raw'; +import { readLiteralBuffers } from '../src/utils/hermes-literals'; +import { + auditRawHermesBytecode, + readHermesSemanticData, +} from '../src/utils/hermes-raw'; const hermesc = process.env.HERMESC; const hasHermesc = Boolean(hermesc && fs.existsSync(hermesc)); @@ -272,13 +276,28 @@ describe.if(hasHermesc)('lossless Hermes operand audit (real compiler)', () => { expect(first).not.toBe(sixth); bytes.writeInt32LE(sixth, start); bytes.writeInt32LE(first, start + 20); - const result = await compareHermesBytecode( + const swapped = rewrite(delta, bytes); + // the pretty pass already catches it by the table's label order... + const result = await compareHermesBytecode(hermesc!, swapped, plain); + expect(result.status).toBe('different'); + // ...and the raw audit, which decodes the targets from the binary table, + // must reject it on its own too + const files: [string, string] = [swapped, plain]; + const audit = await auditRawHermesBytecode( hermesc!, - rewrite(delta, bytes), - plain, + files, + [ + await readHermesSemanticData(swapped), + await readHermesSemanticData(plain), + ], + [ + (await readLiteralBuffers(swapped))!, + (await readLiteralBuffers(plain))!, + ], + new AbortController().signal, ); - expect(result.status).toBe('different'); - expect(result.detail).toContain('raw instruction'); + expect(audit.status).toBe('different'); + expect(audit.detail).toContain('raw instruction'); }); // hermesc annotates the string operand of DefineOwnByIdLong but not of