diff --git a/src/server/verify.js b/src/server/verify.js index c1f7568..f0a2b1a 100644 --- a/src/server/verify.js +++ b/src/server/verify.js @@ -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] }; diff --git a/src/shared/check.js b/src/shared/check.js index 78c4f66..57af627 100644 --- a/src/shared/check.js +++ b/src/shared/check.js @@ -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) + '/'); diff --git a/src/shared/compare.js b/src/shared/compare.js index 7d7e83e..73f0cb3 100644 --- a/src/shared/compare.js +++ b/src/shared/compare.js @@ -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); }