fix(managed): retain raw CIL changes in member comparison (#2912)

This commit is contained in:
Devulapalli Naga Sri Vaishnavi
2026-10-14 00:37:12 +05:30
committed by GitHub
parent 4758ba30b6
commit a9195c3a98
4 changed files with 78 additions and 9 deletions
+12 -7
View File
@@ -209,9 +209,11 @@ native/runtime questions retain their own authority and coverage.
Names and semantic labels do not establish durable member identity. Comparison
preserves observed differences, structural inferences, and unknowns separately.
Cross-version matching first prefers exact CIL/signature identity. When that
does not match, it pairs an exact declared type, method name, and raw signature
before trying structural body shape. The exact-signature key uses names only
Cross-version matching first prefers exact raw CIL/signature identity, with
equal decoded fingerprints. Equal decoded fingerprints alone cannot establish
an exact match: they omit switch targets and collapse distinct NaN encodings.
When that does not match, it pairs an exact declared type, method name, and raw
signature before trying structural body shape. The exact-signature key uses names only
as part of that full tuple; names alone never select a pair. Duplicate tuples
remain ambiguous rather than being paired by token order. Fields follow the
same exact tiers. The raw signature is exact whether or not REA decoded it, so
@@ -222,8 +224,11 @@ unpaired member, one-sided or ambiguous, with the same declared type and name
whose signature was not decoded.
For a matched method, an unavailable or partial body makes body-shape facets
unknown while preserving observed signature differences. Structural identity
compares normalized signatures, the limited decoded-CIL tuple fingerprint,
unknown while preserving observed signature differences. For complete bodies,
a difference in either the raw CIL digest or the decoded fingerprint is reported
in the `cil` dimension, including reordered switch targets with equal decoded
fingerprints. Structural identity compares normalized signatures, the limited
decoded-CIL tuple fingerprint,
constants, and bounded shape context. It reports all candidates at the winning
score and remains ambiguous when the evidence does not distinguish them. The
digest does not itself remap metadata tokens.
@@ -360,8 +365,8 @@ Opcode names retain short/long forms and prefix opcodes. This makes the value a
reproducible decoded-tuple fingerprint, not a canonical semantic CIL identity:
it does not resolve tokens across MVIDs, does not commit full control flow or
exception semantics, and does not replace the raw CIL digest. Cross-build
comparison uses the value only as one unique-only structural tier and reports
ambiguity when that evidence is insufficient.
comparison requires equal raw CIL digests as well for exact CIL/signature
matches and reports ambiguity when structural evidence is insufficient.
For example, a decoded `nop; ret` body serializes exactly as
`[["nop","none",null],["ret","none",null]]` and has digest
@@ -73,7 +73,11 @@ const methodDimensions = (
if (bothAbsent) return { dimensions, bodyUnknown: false };
if (!leftBodyComplete || !rightBodyComplete)
return { dimensions, bodyUnknown: true };
if (left.body.normalized_il_sha256 !== right.body.normalized_il_sha256)
// The decoded fingerprint omits switch targets and is not exact CIL identity.
if (
left.body.il_sha256 !== right.body.il_sha256 ||
left.body.normalized_il_sha256 !== right.body.normalized_il_sha256
)
dimensions.push("cil");
if (
canonicalize(left.body.opcode_counts) !==
@@ -49,10 +49,13 @@ interface Ambiguous<Item> {
const keyMethod = (item: Method): Keyed<Method> => ({
item,
exactKey:
item.body.status === "present" && item.body.normalized_il_sha256 !== null
item.body.status === "present" &&
item.body.il_sha256 !== null &&
item.body.normalized_il_sha256 !== null
? sha256([
"method-exact",
item.signature.raw_sha256,
item.body.il_sha256,
item.body.normalized_il_sha256,
])
: null,
@@ -116,6 +116,63 @@ describe("managed member comparison", () => {
});
});
describe("managed member comparison with equal decoded fingerprints", () => {
it.each([
{ name: "Main", basis: "exact-signature" },
{ name: "Renamed", basis: "structural-method-shape" },
])("detects reordered switch targets in $name", ({ name, basis }) => {
const observe = (targets: readonly number[], methodName: string) => {
// ldarg.0; switch (case 0, case 1); ldc.i4.0; ret; ldc.i4.1; ret.
// Both cases target instruction starts, relative to the end of the table.
const il = Buffer.alloc(18);
il[0] = 0x02;
il[1] = 0x45;
il.writeUInt32LE(2, 2);
targets.forEach((target, index) =>
il.writeInt32LE(target, 6 + index * 4),
);
Buffer.from([0x16, 0x2a, 0x17, 0x2a]).copy(il, 14);
return inspect(
buildManagedPeFixture({
methodName,
methodSignature: Buffer.from([0x00, 0x01, 0x08, 0x08]),
ilBody: Buffer.concat([Buffer.from([(il.length << 2) | 2]), il]),
}),
`/tmp/${methodName}-${targets.join("-")}.dll`,
);
};
const left = observe([0, 2], "Main");
const right = observe([2, 0], name);
const leftBody = left.result.methods[0]?.body;
const rightBody = right.result.methods[0]?.body;
expect(leftBody?.status).toBe("present");
expect(rightBody?.status).toBe("present");
expect(leftBody?.normalized_il_sha256).toBe(
rightBody?.normalized_il_sha256,
);
expect(leftBody?.il_sha256).not.toBe(rightBody?.il_sha256);
const result = compareManagedMembers(left, right);
expect(result.matching.exact_il_signature).toBe(0);
expect(result.methods[0]).toMatchObject({
status: "changed",
match: { status: "matched", basis },
dimensions: ["cil"],
});
expect(
compareManagedMembers(right, observe([2, 0], name)).methods[0],
).toMatchObject({
status: "unchanged",
match: {
status: "matched",
basis: "exact-il-signature",
confidence: "exact",
},
dimensions: [],
});
});
});
describe("managed member comparison floating-point constants", () => {
it.each([
{