fix(managed): honor native platform symbol lookup

Restrict Windows A/W suffix and stdcall candidates to their applicable native format. Preserve original metadata and per-observation Evidence ownership. Exercise identity contradictions and wrong-role controls through the shared CLI and stdio MCP workflows.

Fixes #6926
This commit is contained in:
morluto
2026-10-24 07:55:16 +08:00
parent 79805f244a
commit 64fa1db130
4 changed files with 312 additions and 39 deletions
+6 -1
View File
@@ -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
@@ -256,7 +256,14 @@ const candidateNames = (
): readonly string[] => {
if (managed.import_name.length === 0) return [];
const names = new Set<string>([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")
@@ -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,
@@ -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();