Compare linked code without line numbers, namespace hashes or unlearned names
effectiveHash masks only relocated fields (an operand equal to its source line no longer counts), and normalizes MSVC anonymous-namespace hashes. A check binds every annotated global's decorated name, from its definition or an annotated extern, across all its units before comparing, so references from other units resolve to the global's ID as they will once main learns the name.
This commit is contained in:
parent
64b892b2b0
commit
8d472dd8ee
3 changed files with 44 additions and 10 deletions
|
|
@ -202,7 +202,7 @@ export class Verifier {
|
|||
// The macro-proof lint needs the /P pass: for submitted units; for every unit when a header
|
||||
// changed (a macro there expands everywhere); never for a rebuild of trusted main.
|
||||
lintUnits,
|
||||
analyze: (args) => analyzeInWorkers({ ...args, symbols, nameIndex, peFile: v.file }),
|
||||
analyze: (args) => analyzeInWorkers({ symbols, nameIndex, ...args, peFile: v.file }),
|
||||
});
|
||||
const rec = reconcile(check, symbols);
|
||||
return { check, rec, symbols, changedHeaders, units: [...units] };
|
||||
|
|
|
|||
|
|
@ -34,12 +34,39 @@ export async function checkUnits({ repoDir, jobDir, project, units, overrides =
|
|||
const compiled = await compileUnits({ jobDir, units, project, runners, timeoutMs, preprocess: (f) => !lintUnits || lintUnits.has(f) });
|
||||
|
||||
const roots = { sourceDirs: project.sourceDirs, includeDirs: project.includeDirs };
|
||||
const names = withGlobalBindings(compiled, scan.annotations, nameIndex);
|
||||
const results = analyze
|
||||
? await analyze({ compiled, annotations: scan.annotations, roots })
|
||||
: analyzeCompiled({ compiled, annotations: scan.annotations, roots, symbols, nameIndex, pe });
|
||||
? await analyze({ compiled, annotations: scan.annotations, roots, nameIndex: names })
|
||||
: analyzeCompiled({ compiled, annotations: scan.annotations, roots, symbols, nameIndex: names, pe });
|
||||
return { annotationErrors, units: results, annotations: scan.annotations };
|
||||
}
|
||||
|
||||
/**
|
||||
* A GLOBAL annotation binds its variable's decorated name in the unit that defines it, or that
|
||||
* declares it (an annotated extern: the listing names it as EXTRN). References from the other
|
||||
* units of the same check resolve to that ID too, as they do on main once the name is learned:
|
||||
* the name index the units are compared with includes them.
|
||||
*/
|
||||
export function withGlobalBindings(compiled, annotations, nameIndex) {
|
||||
const globals = annotations.filter((a) => a.kind === 'GLOBAL' && a.name);
|
||||
if (!globals.length) return nameIndex;
|
||||
const byFile = new Map();
|
||||
for (const a of globals) byFile.set(a.file, [...(byFile.get(a.file) ?? []), a]);
|
||||
const out = new Map(nameIndex);
|
||||
for (const c of compiled) {
|
||||
if (!c.ok || !c.cod) continue;
|
||||
const anns = [...(byFile.get(c.file) ?? []), ...(c.includes ?? []).flatMap((h) => byFile.get(h) ?? [])];
|
||||
if (!anns.length) continue;
|
||||
const listing = parseListing(fs.readFileSync(c.cod, 'latin1'));
|
||||
for (const a of anns) {
|
||||
const matches = (d) => d === a.name || (d && d.endsWith('::' + a.name));
|
||||
const hit = listing.data.find((d) => matches(d.demangled))?.symbol ?? [...listing.names].find(([sym, d]) => sym.startsWith('?') && matches(d))?.[0];
|
||||
if (hit && !out.has(hit)) out.set(hit, a.id);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Lints and compares the compiler output of each unit against the original. */
|
||||
export function analyzeCompiled({ compiled, annotations, roots, symbols, nameIndex, pe }) {
|
||||
const projectRoots = [...roots.sourceDirs, ...roots.includeDirs].map((d) => '/' + normalizePath(d) + '/');
|
||||
|
|
|
|||
|
|
@ -298,10 +298,15 @@ export function compareFunction({ fn, addr, origSize, pe, resolve, symbols, proc
|
|||
if (!isSection) inferred.push({ symbol: symName, addr: actual });
|
||||
}
|
||||
relocResults.push({ offset: r.offset, symbol: symName, id, ok, unresolved: ok && !(target && target.addr != null), actual, reason });
|
||||
effective.push(`${r.offset}:${r.kind}:${target && target.addr != null ? wrap(target.addr + addend) : `${symName}+${addend}`}`);
|
||||
// MSVC names an anonymous namespace after the unit's contents (?A0x0c4fce31): editing the file
|
||||
// renames its file-local symbols, which is no change to the code that calls them.
|
||||
const stable = symName.replace(/\?A0x[0-9a-f]{8}@/g, '?A@');
|
||||
effective.push(`${r.offset}:${r.kind}:${target && target.addr != null ? wrap(target.addr + addend) : `?${stable}+${addend}`}`);
|
||||
}
|
||||
|
||||
const sameLength = !origSize || origSize === ours.length;
|
||||
// Relocated fields only, before line numbers are masked against the original (effectiveHash).
|
||||
const relocMasked = Uint8Array.from(masked);
|
||||
if (sameLength && proc) maskLineNumbers({ ours, orig, masked, proc });
|
||||
|
||||
// Byte comparison outside relocated fields.
|
||||
|
|
@ -337,7 +342,7 @@ export function compareFunction({ fn, addr, origSize, pe, resolve, symbols, proc
|
|||
score: Math.round(score * 10000) / 10000,
|
||||
size: { ours: ours.length, original: origSize ?? null },
|
||||
codeHash: codeHash(ours, proc),
|
||||
effectiveHash: effectiveHash(ours, masked, proc, effective),
|
||||
effectiveHash: effectiveHash(ours, relocMasked, effective),
|
||||
diffRegions: sameLength ? diffRegions(diffs, ours, orig, proc) : [],
|
||||
relocations: relocResults.filter((r) => !r.ok || r.unresolved),
|
||||
// Where the original points is only meaningful when the code lines up.
|
||||
|
|
@ -457,12 +462,14 @@ export function codeHash(bytes, proc) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Fingerprint of what a function does once linked: its bytes outside relocated fields (as
|
||||
* codeHash) plus where each reference lands. Renaming a global, or reaching a member of an object
|
||||
* through the object's symbol instead of a symbol of its own at that address, leaves it unchanged.
|
||||
* Fingerprint of what a function does once linked: its bytes outside relocated fields plus where
|
||||
* each reference lands. Renaming a global, or reaching a member of an object through the object's
|
||||
* symbol instead of a symbol of its own at that address, leaves it unchanged. Source lines play no
|
||||
* part (unlike codeHash): an operand that happens to equal its line number would make moving the
|
||||
* function look like changing it.
|
||||
*/
|
||||
export function effectiveHash(bytes, masked, proc, effective) {
|
||||
export function effectiveHash(bytes, masked, effective) {
|
||||
const b = Buffer.from(bytes);
|
||||
for (let i = 0; i < b.length; i++) if (masked[i]) b[i] = 0;
|
||||
return crypto.createHash('sha1').update(codeHash(b, proc)).update(effective.join('|')).digest('hex').slice(0, 20);
|
||||
return crypto.createHash('sha1').update(b).update(effective.join('|')).digest('hex').slice(0, 20);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue