mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user