From d1e5c0e1fd04d2d5a0d663cbe43787a010e3d783 Mon Sep 17 00:00:00 2001 From: Rudy Mizrahi Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Mon, 5 Oct 2026 13:51:59 -0400 Subject: [PATCH] fix(comparison): normalize CFG identity by numeric addresses (#593) Equivalent hexadecimal addresses no longer collapse a fully observed CFG to unknown, and duplicate numeric block starts are rejected even when spelled differently. --- src/domain/functionComparison.test.ts | 43 +++++++++++++++++++ src/domain/functionComparisonNormalization.ts | 42 ++++++++++-------- 2 files changed, 67 insertions(+), 18 deletions(-) diff --git a/src/domain/functionComparison.test.ts b/src/domain/functionComparison.test.ts index 100ae4df5e..6b6f78a506 100644 --- a/src/domain/functionComparison.test.ts +++ b/src/domain/functionComparison.test.ts @@ -102,6 +102,49 @@ const observe = ( }, ); +describe("function comparison CFG address normalization", () => { + it("matches CFG successors by numeric address", () => { + const make = (base: "0x1000" | "0x2000") => + functionDossierSchema.parse({ + ...dossier("return 0;", base), + basic_blocks: [ + { + start: base, + end: base === "0x1000" ? "0x1004" : "0x2004", + successors: [base === "0x1000" ? "0X01004" : "0x2004"], + }, + { + start: base === "0x1000" ? "0x1004" : "0x2004", + end: base === "0x1000" ? "0x1008" : "0x2008", + successors: [], + }, + ], + }); + expect( + compareFunctions( + observe("b", make("0x1000")), + observe("c", make("0x2000")), + ).dimensions.find(({ dimension }) => dimension === "cfg"), + ).toMatchObject({ status: "unchanged" }); + }); + + it("keeps duplicate numeric block starts unknown", () => { + const duplicate = functionDossierSchema.parse({ + ...dossier("return 0;", "0x1000"), + basic_blocks: [ + { start: "0x1000", end: "0x1004", successors: [] }, + { start: "0X01000", end: "0x1008", successors: [] }, + ], + }); + expect( + compareFunctions( + observe("d", duplicate), + observe("e", duplicate), + ).dimensions.find(({ dimension }) => dimension === "cfg"), + ).toMatchObject({ status: "unknown" }); + }); +}); + describe("function comparison", () => { it("preserves text changes alongside complete inline dossier facets", () => { const result = compareFunctions( diff --git a/src/domain/functionComparisonNormalization.ts b/src/domain/functionComparisonNormalization.ts index 00859234f6..1d7dce3f84 100644 --- a/src/domain/functionComparisonNormalization.ts +++ b/src/domain/functionComparisonNormalization.ts @@ -40,30 +40,36 @@ export const functionMatch = ( export const normalizeCfg = ( blocks: FunctionSnapshot["collections"]["basic_blocks"]["items"], ): readonly unknown[] | null => { - const parsed = blocks.map((block) => ({ - block, - start: parseAddress(block.start), - end: parseAddress(block.end), - })); - if (parsed.some(({ start, end }) => start === null || end === null)) - return null; + const parsed: { + block: (typeof blocks)[number]; + start: bigint; + end: bigint; + }[] = []; + for (const block of blocks) { + const start = parseAddress(block.start); + const end = parseAddress(block.end); + if (start === null || end === null || end <= start) return null; + parsed.push({ block, start, end }); + } const ordered = parsed.sort((left, right) => - (left.start ?? 0n) < (right.start ?? 0n) ? -1 : 1, + left.start < right.start ? -1 : left.start > right.start ? 1 : 0, ); - if (new Set(ordered.map(({ block }) => block.start)).size !== ordered.length) + if (new Set(ordered.map(({ start }) => start)).size !== ordered.length) return null; - if (ordered.some(({ start, end }) => (end ?? 0n) <= (start ?? 0n))) - return null; - const indices = new Map( - ordered.map(({ block }, index) => [block.start, index]), - ); + const indices = new Map(ordered.map(({ start }, index) => [start, index])); const graph = []; for (const { block, start, end } of ordered) { - const successors = block.successors.map((address) => indices.get(address)); - if (successors.some((index) => index === undefined)) return null; + const successors: number[] = []; + for (const address of block.successors) { + const parsedAddress = parseAddress(address); + if (parsedAddress === null) return null; + const index = indices.get(parsedAddress); + if (index === undefined) return null; + successors.push(index); + } graph.push({ - size: String((end ?? 0n) - (start ?? 0n)), - successors: successors.sort((left, right) => (left ?? 0) - (right ?? 0)), + size: String(end - start), + successors: successors.sort((left, right) => left - right), }); } return graph;