fix(managed): preserve native export coverage during verification (#9472)

This commit is contained in:
morluto authored and GitHub committed 2026-11-09 06:03:48 +08:00
1 parent 90c95a6b64
commit 00a6524b9a
3 files changed
+94 -2

No files matched your search

@@ -20,12 +20,16 @@ export const collectNativeSymbols = (
readonly symbols: readonly NativeSymbol[];
readonly accepted: number;
readonly unsupported: number;
readonly partialExports: boolean;
readonly exportLimitations: readonly string[];
readonly moduleFormatsByEvidenceId: ReadonlyMap<string, string | null>;
} => {
const symbols: NativeSymbol[] = [];
const moduleFormatsByEvidenceId = new Map<string, string | null>();
let accepted = 0;
let unsupported = 0;
let partialExports = false;
const exportLimitations: string[] = [];
for (const observation of observations) {
moduleFormatsByEvidenceId.set(
observation.evidence.evidence_id,
@@ -36,9 +40,23 @@ export const collectNativeSymbols = (
} else {
accepted += 1;
symbols.push(...observation.symbols);
if (observation.exportCoverage !== undefined) {
if (!observation.exportCoverage.exhaustive) partialExports = true;
for (const limitation of observation.exportCoverage.limitations)
exportLimitations.push(
`Native export Evidence ${observation.evidence.evidence_id}: ${limitation}`,
);
}
}
}
return { symbols, accepted, unsupported, moduleFormatsByEvidenceId };
return {
symbols,
accepted,
unsupported,
partialExports,
exportLimitations,
moduleFormatsByEvidenceId,
};
};
/** P/Invoke resolves a Mach-O export through dlsym, which names it without the symbol-table prefix. */
@@ -516,6 +534,8 @@ interface VerificationResultInput {
readonly symbols: readonly NativeSymbol[];
readonly accepted: number;
readonly unsupported: number;
readonly partialExports: boolean;
readonly exportLimitations: readonly string[];
};
readonly pinvokeImports: readonly PinvokeVerification[];
readonly nativeObservationCount: number;
@@ -540,7 +560,9 @@ export const buildVerificationResult = ({
).length;
const coverage: ManagedNativeVerificationResult["coverage"] = {
status:
managed.coverage.state === "complete" && native.unsupported === 0
managed.coverage.state === "complete" &&
native.unsupported === 0 &&
!native.partialExports
? "complete-within-inputs"
: "partial",
};
@@ -594,6 +616,10 @@ export const buildVerificationResult = ({
: [
"Some native Evidence operations were unsupported by this workflow.",
]),
...(native.partialExports
? ["Some native export inventories are not exhaustive."]
: []),
...native.exportLimitations,
],
};
};
@@ -34,6 +34,11 @@ export interface ManagedNativeObservationFacts {
readonly evidence: Evidence;
readonly symbols: readonly NativeSymbol[];
readonly supported: boolean;
/** Coverage of an export inventory; function observations are targeted. */
readonly exportCoverage?: {
readonly exhaustive: boolean;
readonly limitations: readonly string[];
};
}
const inputFailure = (
@@ -62,6 +67,10 @@ const readNativeObservation = (
return ok({
evidence,
supported: true,
exportCoverage: {
exhaustive: payload.value.exports.exhaustive,
limitations: payload.value.exports.limitations,
},
symbols: payload.value.exports.items.map((symbol) => ({
evidence_id: evidence.evidence_id,
operation: evidence.operation,
@@ -176,6 +176,63 @@ it("records a residual question for partial verification with zero item counts",
expect(harness.session.listUnknowns()).toHaveLength(1);
});
it("retains partial native export coverage after an observed match", async () => {
const harness = await createApplicationMcpHarness({
session: createTestBinarySession(() => {
throw new Error("No provider needed");
}),
});
onTestFinished(harness.close);
const original = MANAGED_NATIVE_VERIFICATION_EXAMPLE.native_observations[0];
if (original === undefined || original.subject === null)
throw new Error("Managed verification example is missing native Evidence");
const report = inspectMachoSchema.parse(original.normalized_result);
const native = createEvidence(
{
path: original.subject.local_path!,
sha256: original.subject.digest.sha256,
format: original.subject.format,
},
original.provider,
{
operation: "inspect_macho",
parameters: original.parameters,
result: {
...report,
exports: {
...report.exports,
exhaustive: false,
limitations: ["Export inventory may be incomplete."],
},
},
rawResult: null,
},
);
const response = await harness.client.callTool({
name: "verify_managed_native_boundaries",
arguments: {
managed_boundaries:
MANAGED_NATIVE_VERIFICATION_EXAMPLE.managed_boundaries,
native_observations: [native],
},
});
expect(response.isError, JSON.stringify(response.content)).not.toBe(true);
const evidence = parseEvidence(response.structuredContent);
const result = managedNativeVerificationResultSchema.parse(
evidence.normalized_result,
);
expect(result.summary).toMatchObject({
verified: 1,
unresolved: 0,
contradicted: 0,
});
expect(result.coverage.status).toBe("partial");
expect(result.limitations).toContainEqual(
expect.stringContaining("Export inventory may be incomplete."),
);
expect(harness.session.listUnknowns()).toHaveLength(1);
});
it("records distinct unresolved managed boundary checks and makes exact replay idempotent", async () => {
const harness = await createApplicationMcpHarness({
session: createTestBinarySession(() => {