diff --git a/docs/managed-code-analysis.md b/docs/managed-code-analysis.md index b35374f2ac..e1b5b255d8 100644 --- a/docs/managed-code-analysis.md +++ b/docs/managed-code-analysis.md @@ -552,7 +552,12 @@ An empty declared import name remains unchanged in verification Evidence and produces no native-name candidates. With a supplied PE native context and `no_mangle: false`, an unspecified charset includes the ANSI `A` candidate; `auto` includes the Windows Unicode `W` candidate. Other native formats do not -inherit these Windows defaults. These suffixes are static inferred candidates; +inherit these Windows defaults. Known ELF and Mach-O contexts use the declared +entry point without A/W suffixes or Windows stdcall decoration, including when +the metadata declares an explicit ANSI or Unicode marshalling charset. The +original charset and calling convention remain unchanged. This follows the +[.NET runtime's platform-specific entry-point lookup](https://github.com/dotnet/runtime/blob/v10.0.0/src/coreclr/vm/method.cpp#L3134-L3177). +Windows name decorations are static inferred candidates; exact spelling still suppresses decoration and no runtime binding is proven. The mapping follows [Microsoft's charset guidance](https://learn.microsoft.com/en-us/dotnet/standard/native-interop/charset). Partial input coverage records a residual question even when no unresolved diff --git a/src/domain/managed/managedNativeVerificationMatch.ts b/src/domain/managed/managedNativeVerificationMatch.ts index 240e8a6456..ee48579a25 100644 --- a/src/domain/managed/managedNativeVerificationMatch.ts +++ b/src/domain/managed/managedNativeVerificationMatch.ts @@ -256,7 +256,14 @@ const candidateNames = ( ): readonly string[] => { if (managed.import_name.length === 0) return []; const names = new Set([managed.import_name]); - if (!managed.no_mangle) { + // Unix P/Invoke lookup uses the declared entry point directly, regardless of + // marshalling charset or calling convention. Keep Windows decorations local + // to PE or an unresolved native platform; metadata flags remain unchanged. + if ( + !managed.no_mangle && + nativeFormat !== "elf" && + nativeFormat !== "mach-o" + ) { if ( managed.char_set === "unicode" || (nativeFormat === "pe" && managed.char_set === "auto") diff --git a/tests/boundary/mcp/managedNativeVerificationAdapters.test.ts b/tests/boundary/mcp/managedNativeVerificationAdapters.test.ts index b5e9754aac..8a76ea7ceb 100644 --- a/tests/boundary/mcp/managedNativeVerificationAdapters.test.ts +++ b/tests/boundary/mcp/managedNativeVerificationAdapters.test.ts @@ -6,6 +6,12 @@ import { afterAll, beforeAll, expect } from "vitest"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { functionDossierSchema } from "../../../src/domain/hopperValues.js"; +import { + MANAGED_APPLICATION_GRAPH_EXAMPLE, + MANAGED_NATIVE_VERIFICATION_EXAMPLE, +} from "../../../src/contracts/managed/managedWorkflowExamples.js"; +import { managedMemberInspectionSchema } from "../../../src/domain/managed/managedArtifact.js"; +import { managedApplicationGraphReferenceInputSchema } from "../../../src/contracts/managed/managedWorkflowToolContracts.js"; import { managedNativeVerificationResultSchema } from "../../../src/domain/managed/managedNativeVerificationSchemas.js"; import { buildManagedPeFixture } from "../../../src/dotnet/ManagedPe.fixture.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; @@ -28,6 +34,136 @@ afterAll(async () => { await Promise.all([client.close(), transport.close()]); }); +cliTest( + "preserves managed graph identity failures and wrong-role controls through CLI and stdio MCP", + async ({ cli }) => { + const original = MANAGED_APPLICATION_GRAPH_EXAMPLE.managed_members; + const members = managedMemberInspectionSchema.parse( + original.normalized_result, + ); + const otherDigest = "b".repeat(64); + const otherMvid = "ffffffff-ffff-ffff-ffff-ffffffffffff"; + for (const variant of [ + "subject-sha256", + "scope-sha256", + "scope-mvid", + ] as const) { + const source = createEvidence( + variant === "subject-sha256" + ? { path: members.artifact.path, format: "pe", sha256: otherDigest } + : undefined, + original.provider, + { + operation: original.operation, + parameters: {}, + result: { + ...members, + identity_scope: { + ...members.identity_scope, + ...(variant === "scope-sha256" + ? { requires_artifact_sha256: otherDigest } + : {}), + ...(variant === "scope-mvid" ? { requires_mvid: otherMvid } : {}), + }, + }, + rawResult: null, + }, + ); + // The record authenticates and the request has the advertised shape; + // rejection must identify the contradictory identities within that record. + const input = managedApplicationGraphReferenceInputSchema.parse({ + managed_members: parseEvidence(JSON.parse(JSON.stringify(source))), + }); + const response = await client.callTool({ + name: "project_managed_application_graph", + arguments: input, + }); + const error = parseMcpToolError(response).error; + const contradiction = + variant === "subject-sha256" + ? `subject SHA-256 ${otherDigest} does not match normalized artifact SHA-256 ${members.artifact.sha256}` + : variant === "scope-sha256" + ? `token scope SHA-256 ${otherDigest} does not match normalized artifact SHA-256 ${members.artifact.sha256}` + : `token scope MVID ${otherMvid} does not match module MVID ${String(members.module?.mvid)}`; + expect(error).toMatchObject({ + code: "evidence_integrity_mismatch", + category: "integrity_mismatch", + retryable: false, + details: { + reason: `Managed Evidence inspect_managed_members (${source.evidence_id}) ${contradiction}`, + }, + }); + expect(error.recovery).toBeUndefined(); + expect(response.structuredContent).toBeUndefined(); + const command = await cli.run({ + arguments: [ + "project-managed-application-graph", + JSON.stringify(input), + "--json", + ], + }); + expect(command.exitCode, command.stderr).toBe(1); + expect(command.json).toMatchObject(error); + expect(command.json).not.toHaveProperty("recovery"); + expect(command.json).not.toHaveProperty("evidence_id"); + } + + const wrongRole = MANAGED_NATIVE_VERIFICATION_EXAMPLE.managed_boundaries; + const wrongInput = { managed_members: wrongRole }; + const wrongResponse = await client.callTool({ + name: "project_managed_application_graph", + arguments: wrongInput, + }); + const wrongError = parseMcpToolError(wrongResponse).error; + expect(wrongError).toMatchObject({ + code: "invalid_request", + category: "invalid_input", + recovery: { + kind: "inspect_input_contract", + operation: "project_managed_application_graph", + }, + details: { + issues: [ + { + path: ["managed_members"], + reason: "invalid_value", + message: `Expected rea.analysis inspect_managed_members Evidence, received ${wrongRole.predicate_type} ${wrongRole.operation}`, + }, + ], + }, + }); + const wrongCommand = await cli.run({ + arguments: [ + "project-managed-application-graph", + JSON.stringify(wrongInput), + "--json", + ], + }); + expect(wrongCommand.exitCode, wrongCommand.stderr).toBe(1); + expect(wrongCommand.json).toMatchObject(wrongError); + + const control = await client.callTool({ + name: "project_managed_application_graph", + arguments: MANAGED_APPLICATION_GRAPH_EXAMPLE, + }); + expect(control.isError, JSON.stringify(control.content)).not.toBe(true); + const commandControl = await cli.run({ + arguments: [ + "project-managed-application-graph", + JSON.stringify(MANAGED_APPLICATION_GRAPH_EXAMPLE), + "--json", + ], + }); + expect(commandControl.exitCode, commandControl.stderr).toBe(0); + const evidence = parseEvidence(control.structuredContent); + expect(parseEvidence(commandControl.json).normalized_result).toEqual( + evidence.normalized_result, + ); + expect(evidence.evidence_links).toEqual([original.evidence_id]); + }, + 60_000, +); + /** Authentic native Evidence whose operation the workflow cannot read symbols from. */ const signatureEvidence = () => createEvidence( @@ -47,8 +183,73 @@ const signatureEvidence = () => }, ); +const platformPinvokeCases = [ + { + format: "pe", + scope: "nativehelper.dll", + module: "nativehelper.dll", + status: "verified", + }, + { + format: "pe", + scope: "nativehelper.dll", + module: "nativehelper.exe", + status: "contradicted", + }, + { + format: "pe", + scope: "nativehelper.dll", + module: "nativehelper.node", + status: "contradicted", + }, + { + format: "pe", + scope: "nativehelper", + module: "nativehelper.dll", + status: "verified", + }, + { + scope: "nativedep.so.6", + module: "libnativedep.so.6", + format: "elf", + status: "verified", + }, + { + scope: "nativehelper", + module: "libnativehelper.so", + format: "elf", + flags: 0x202, + symbol: "open_nativeA", + status: "unresolved", + }, + { + scope: "nativehelper", + module: "libnativehelper.dylib", + format: "mach-o", + flags: 0x204, + symbol: "open_nativeW", + status: "unresolved", + }, + { + scope: "nativehelper", + module: "libnativehelper.so", + format: "elf", + flags: 0x204, + symbol: "open_native", + status: "verified", + }, + { + scope: "nativehelper", + module: "nativehelper.dll", + format: "pe", + flags: 0x202, + symbol: "open_nativeA", + status: "inferred", + }, +] as const; + cliTest( - "preserves explicit PE module suffixes and matches only the Windows DLL variation through CLI and MCP", + "preserves platform-specific P/Invoke names and module matching through CLI and MCP", async ({ cli }) => { const root = await createTestTempDirectory( "rea-managed-native-module-names-", @@ -86,45 +287,17 @@ cliTest( native_value_flow: null, limitations: ["No native provider was executed."], }); - const cases = [ - { - format: "pe", - scope: "nativehelper.dll", - module: "nativehelper.dll", - status: "verified", - }, - { - format: "pe", - scope: "nativehelper.dll", - module: "nativehelper.exe", - status: "contradicted", - }, - { - format: "pe", - scope: "nativehelper.dll", - module: "nativehelper.node", - status: "contradicted", - }, - { - format: "pe", - scope: "nativehelper", - module: "nativehelper.dll", - status: "verified", - }, - { - scope: "nativedep.so.6", - module: "libnativedep.so.6", - format: "elf", - status: "verified", - }, - ] as const; - for (const [index, item] of cases.entries()) { + for (const [index, item] of platformPinvokeCases.entries()) { const managedPath = join(root, `managed-${String(index)}.dll`); await writeFile( managedPath, buildManagedPeFixture({ - pinvoke: { importName: "open_native", moduleName: item.scope }, + pinvoke: { + importName: "open_native", + moduleName: item.scope, + ...("flags" in item ? { mappingFlags: item.flags } : {}), + }, }), ); const inspected = await client.callTool({ @@ -145,7 +318,13 @@ cliTest( { operation: "analyze_function", parameters: {}, - result: dossier, + result: { + ...dossier, + procedure: { + ...dossier.procedure, + name: "symbol" in item ? item.symbol : "open_native", + }, + }, rawResult: null, limitations: ["Synthetic native Evidence; no provider executed."], }, @@ -175,7 +354,18 @@ cliTest( expect(mcpResult.pinvoke_imports[0]).toMatchObject({ status: item.status, ...(item.status === "contradicted" ? { basis: "module-mismatch" } : {}), + ...(item.status === "unresolved" + ? { + basis: "no-native-candidate", + candidates: [], + matched_native: null, + } + : {}), }); + expect(mcpResult.evidence_links).toEqual([ + managed.evidence_id, + native.evidence_id, + ]); } }, 60_000, diff --git a/tests/conformance/providers/managed/managedNativeVerification.test.ts b/tests/conformance/providers/managed/managedNativeVerification.test.ts index defecae410..3171f118a5 100644 --- a/tests/conformance/providers/managed/managedNativeVerification.test.ts +++ b/tests/conformance/providers/managed/managedNativeVerification.test.ts @@ -190,6 +190,22 @@ it.each([ { flags: 0x206, symbol: "Example", format: "pe", status: "verified" }, { flags: 0x200, symbol: "ExampleA", format: "elf", status: "unresolved" }, { flags: 0x206, symbol: "ExampleW", format: "elf", status: "unresolved" }, + { flags: 0x202, symbol: "ExampleA", format: "elf", status: "unresolved" }, + { flags: 0x204, symbol: "ExampleW", format: "elf", status: "unresolved" }, + { flags: 0x204, symbol: "Example", format: "elf", status: "verified" }, + { flags: 0x202, symbol: "ExampleA", format: "mach-o", status: "unresolved" }, + { flags: 0x204, symbol: "ExampleW", format: "mach-o", status: "unresolved" }, + { flags: 0x202, symbol: "Example", format: "mach-o", status: "verified" }, + { flags: 0x300, symbol: "_Example", format: "elf", status: "unresolved" }, + { + flags: 0x300, + symbol: "_Example@0", + format: "mach-o", + status: "unresolved", + }, + { flags: 0x300, symbol: "_Example@0", format: "pe", status: "inferred" }, + { flags: 0x200, symbol: "ExampleA", format: "file", status: "unresolved" }, + { flags: 0x206, symbol: "ExampleW", format: "file", status: "unresolved" }, { flags: 0x200, symbol: "A", @@ -224,7 +240,12 @@ it.each([ }); expect(result.pinvoke_imports[0]).toMatchObject({ status, - managed: { import_name: inspection.pinvoke_imports[0]?.import_name }, + managed: { + import_name: inspection.pinvoke_imports[0]?.import_name, + char_set: inspection.pinvoke_imports[0]?.char_set, + no_mangle: inspection.pinvoke_imports[0]?.no_mangle, + call_convention: inspection.pinvoke_imports[0]?.call_convention, + }, }); expect( result.pinvoke_imports[0]?.candidates.map(({ name }) => name), @@ -232,6 +253,56 @@ it.each([ }, ); +it("keeps Windows name candidates tied to their native Evidence format", () => { + const bytes = buildManagedPeFixture({ + pinvoke: { + mappingFlags: 0x204, + importName: "Example", + moduleName: "nativehelper.dll", + }, + }); + const target = managedPeFixtureTarget(bytes); + const inspection = inspectManagedNativeBoundariesBytes(bytes, target); + const managed = createEvidence( + target, + exampleInput().managed_boundaries.provider, + { + operation: "inspect_managed_native_boundaries", + parameters: {}, + result: inspection, + }, + ); + const elf = nativeFunctionEvidence( + "/fixtures/nativehelper.dll", + "elf", + "ExampleW", + ); + const pe = nativeFunctionEvidence( + "/fixtures/nativehelper.dll", + "pe", + "ExampleW", + ); + const result = verifyManagedNativeBoundaries({ + managed_boundaries: managed, + native_observations: [elf, pe], + }); + expect(result.pinvoke_imports[0]).toMatchObject({ + status: "inferred", + basis: "decorated-name-candidate", + candidates: [{ evidence_id: pe.evidence_id, name: "ExampleW" }], + evidence_links: [managed.evidence_id, pe.evidence_id], + }); + expect(result.evidence_links).toEqual([ + managed.evidence_id, + elf.evidence_id, + pe.evidence_id, + ]); + expect(inspection.pinvoke_imports[0]).toMatchObject({ + mapping_flags: 0x204, + char_set: "unicode", + }); +}); + describe("managed/native boundary Evidence identity", () => { it("rejects managed boundary Evidence whose subject digest differs from its artifact", () => { const input = exampleInput();