Compare commits

...
4 changed files with 285 additions and 95 deletions

No files matched your search

+9 -5
View File
@@ -550,11 +550,15 @@ verification Evidence without this separate count leaves it unknown (`null`).
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;
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).
`no_mangle: false`, ANSI or an unspecified charset includes the `A` candidate;
Unicode or `auto` includes the `W` candidate. Windows stdcall spelling candidates
also require PE context. ELF, Mach-O and unknown native formats retain the
declared name without adding Windows spelling variants, including for explicit
charsets. Normalization of a Mach-O export's leading underscore remains separate.
These are static inferred candidates; exact spelling still suppresses decoration
and no runtime binding or ABI equivalence is proven. The mapping follows
[Microsoft's charset guidance](https://learn.microsoft.com/en-us/dotnet/standard/native-interop/charset)
and [.NET's platform-specific entry-point lookup](https://github.com/dotnet/runtime/blob/v10.0.0/src/coreclr/vm/method.cpp#L3134-L3177).
Partial input coverage records a residual question even when no unresolved
individual boundary was produced. Selected literal inventories likewise remain
partial when unknown MethodDef ownership can hide a selected body.
@@ -256,16 +256,12 @@ const candidateNames = (
): readonly string[] => {
if (managed.import_name.length === 0) return [];
const names = new Set<string>([managed.import_name]);
if (!managed.no_mangle) {
if (
managed.char_set === "unicode" ||
(nativeFormat === "pe" && managed.char_set === "auto")
)
// A/W probes and stdcall spellings belong to the Windows loader. Charset
// marshalling alone cannot establish name probing for another native format.
if (!managed.no_mangle && nativeFormat === "pe") {
if (managed.char_set === "unicode" || managed.char_set === "auto")
names.add(`${managed.import_name}W`);
if (
managed.char_set === "ansi" ||
(nativeFormat === "pe" && managed.char_set === "not-specified")
)
if (managed.char_set === "ansi" || managed.char_set === "not-specified")
names.add(`${managed.import_name}A`);
if (managed.call_convention === "stdcall") {
names.add(`_${managed.import_name}`);
@@ -6,7 +6,10 @@ import { afterAll, beforeAll, expect } from "vitest";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { functionDossierSchema } from "../../../src/domain/hopperValues.js";
import { managedNativeVerificationResultSchema } from "../../../src/domain/managed/managedNativeVerificationSchemas.js";
import {
managedNativeVerificationResultSchema,
type ManagedNativeVerificationResult,
} from "../../../src/domain/managed/managedNativeVerificationSchemas.js";
import { buildManagedPeFixture } from "../../../src/dotnet/ManagedPe.fixture.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
@@ -47,84 +50,156 @@ const signatureEvidence = () =>
},
);
cliTest(
"preserves explicit PE module suffixes and matches only the Windows DLL variation through CLI and MCP",
async ({ cli }) => {
const root = await createTestTempDirectory(
"rea-managed-native-module-names-",
);
const nativeProvider = {
id: "native-fixture",
name: "Synthetic native fixture",
version: "1",
};
const dossier = functionDossierSchema.parse({
procedure: {
address: "0x1000",
name: "open_native",
classification: {
external: false,
thunk: false,
thunk_target: null,
provenance: "synthetic-provider",
},
body: { available: false, reason: "Synthetic test Evidence only." },
signature: null,
locals: [],
},
pseudocode: "",
assembly: [],
comments: [],
callers: [],
callees: [],
incoming_references: [],
outgoing_references: [],
referenced_strings: [],
referenced_names: [],
basic_blocks: [],
native_api: null,
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;
type PinvokeOutcome =
ManagedNativeVerificationResult["pinvoke_imports"][number];
type AdapterCase = {
scope: string;
module: string;
format?: "pe" | "elf" | "mach-o" | "file";
status: PinvokeOutcome["status"];
nativeName?: string;
mappingFlags?: number;
basis?: PinvokeOutcome["basis"];
};
for (const [index, item] of cases.entries()) {
const nativeCases: readonly AdapterCase[] = [
{
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",
nativeName: "open_nativeA",
mappingFlags: 0x0202,
status: "unresolved",
basis: "no-native-candidate",
},
{
scope: "nativehelper",
module: "libnativehelper.so",
format: "elf",
nativeName: "open_nativeW",
mappingFlags: 0x0204,
status: "unresolved",
basis: "no-native-candidate",
},
{
scope: "nativehelper.dll",
module: "nativehelper.dll",
format: "pe",
nativeName: "open_nativeA",
mappingFlags: 0x0202,
status: "inferred",
basis: "decorated-name-candidate",
},
{
scope: "nativehelper.dll",
module: "nativehelper.dll",
format: "pe",
nativeName: "open_nativeW",
mappingFlags: 0x0204,
status: "inferred",
basis: "decorated-name-candidate",
},
{
scope: "nativehelper.dll",
module: "nativehelper.dll",
format: "pe",
nativeName: "open_nativeW",
mappingFlags: 0x0205,
status: "unresolved",
basis: "no-native-candidate",
},
{
scope: "nativehelper",
module: "libnativehelper.so",
format: "elf",
nativeName: "_open_native@0",
mappingFlags: 0x0300,
status: "unresolved",
basis: "no-native-candidate",
},
];
for (const [index, item] of nativeCases.entries()) {
cliTest(
`matches ${item.scope} to ${item.module} using ${item.nativeName ?? "open_native"} (flags ${item.mappingFlags?.toString(16) ?? "default"}) through CLI and MCP`,
async ({ cli }) => {
const root = await createTestTempDirectory(
"rea-managed-native-module-names-",
);
const nativeProvider = {
id: "native-fixture",
name: "Synthetic native fixture",
version: "1",
};
const dossier = functionDossierSchema.parse({
procedure: {
address: "0x1000",
name: "open_native",
classification: {
external: false,
thunk: false,
thunk_target: null,
provenance: "synthetic-provider",
},
body: { available: false, reason: "Synthetic test Evidence only." },
signature: null,
locals: [],
},
pseudocode: "",
assembly: [],
comments: [],
callers: [],
callees: [],
incoming_references: [],
outgoing_references: [],
referenced_strings: [],
referenced_names: [],
basic_blocks: [],
native_api: null,
native_value_flow: null,
limitations: ["No native provider was executed."],
});
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,
...(item.mappingFlags === undefined
? {}
: { mappingFlags: item.mappingFlags }),
},
}),
);
const inspected = await client.callTool({
@@ -145,7 +220,13 @@ cliTest(
{
operation: "analyze_function",
parameters: {},
result: dossier,
result: {
...dossier,
procedure: {
...dossier.procedure,
name: item.nativeName ?? dossier.procedure.name,
},
},
rawResult: null,
limitations: ["Synthetic native Evidence; no provider executed."],
},
@@ -175,11 +256,22 @@ cliTest(
expect(mcpResult.pinvoke_imports[0]).toMatchObject({
status: item.status,
...(item.status === "contradicted" ? { basis: "module-mismatch" } : {}),
...(item.basis === undefined ? {} : { basis: item.basis }),
});
}
},
60_000,
);
if (item.basis === "no-native-candidate")
expect(mcpResult.pinvoke_imports[0]).toMatchObject({
confidence: "unknown",
matched_native: null,
candidates: [],
});
if (item.status === "inferred")
expect(mcpResult.pinvoke_imports[0]).toMatchObject({
confidence: "inferred",
matched_native: { name: item.nativeName },
});
},
);
}
cliTest(
"counts authentic unsupported native Evidence identically through MCP and CLI",
@@ -17,7 +17,10 @@ import {
managedNativeVerificationInputSchema,
managedNativeVerificationResultSchema,
} from "../../../../src/domain/managed/managedNativeVerificationSchemas.js";
import { managedNativeBoundaryInspectionSchema } from "../../../../src/domain/managed/managedArtifact.js";
import {
managedNativeBoundaryInspectionSchema,
type ManagedNativeBoundaryInspection,
} from "../../../../src/domain/managed/managedArtifact.js";
import { inspectMachoSchema } from "../../../../src/domain/native/nativeInspection.js";
import { functionDossierSchema } from "../../../../src/domain/hopperValues.js";
@@ -124,7 +127,15 @@ const nativeFunctionEvidence = (
);
};
const inputWithImportScope = (scope: string | null) => {
type PinvokeDeclaration =
ManagedNativeBoundaryInspection["pinvoke_imports"][number];
const inputWithImportScope = (
scope: string | null,
declaration: Partial<
Pick<PinvokeDeclaration, "char_set" | "no_mangle" | "call_convention">
> = {},
) => {
const input = exampleInput();
const managed = input.managed_boundaries;
const normalized = managedNativeBoundaryInspectionSchema.parse(
@@ -140,6 +151,7 @@ const inputWithImportScope = (scope: string | null) => {
pinvoke_imports: normalized.pinvoke_imports.map((item) => ({
...item,
import_scope_name: scope,
...declaration,
})),
},
rawResult: null,
@@ -478,6 +490,92 @@ it("keeps explicit PE module suffixes distinct and permits the Windows DLL varia
).toMatchObject({ status: "contradicted", basis: "module-mismatch" });
});
describe.each([
{ format: "pe", path: "/examples/nativehelper.dll", decorated: true },
{ format: "elf", path: "/examples/libnativehelper.so", decorated: false },
{
format: "mach-o",
path: "/examples/libnativehelper.dylib",
decorated: false,
},
{ format: "file", path: "/examples/nativehelper", decorated: false },
] as const)(
"P/Invoke entry-point naming for $format",
({ format, path, decorated }) => {
it.each([
["ansi", "cdecl", "open_nativeA"],
["unicode", "cdecl", "open_nativeW"],
["not-specified", "cdecl", "open_nativeA"],
["auto", "cdecl", "open_nativeW"],
["not-specified", "stdcall", "_open_native"],
["not-specified", "stdcall", "_open_native@0"],
] as const)(
"classifies %s %s candidate %s",
(charSet, convention, name) => {
const result = verifyManagedNativeBoundaries({
...inputWithImportScope(path, {
char_set: charSet,
no_mangle: false,
call_convention: convention,
}),
native_observations: [nativeFunctionEvidence(path, format, name)],
}).pinvoke_imports[0];
expect(result?.managed).toMatchObject({
char_set: charSet,
call_convention: convention,
no_mangle: false,
});
expect(result).toMatchObject(
decorated
? {
status: "inferred",
basis: "decorated-name-candidate",
confidence: "inferred",
matched_native: { name },
}
: {
status: "unresolved",
basis: "no-native-candidate",
confidence: "unknown",
matched_native: null,
candidates: [],
},
);
},
);
},
);
it.each([
["_open_native", "verified", "exact-export-name"],
["__open_native", "unresolved", "no-native-candidate"],
] as const)(
"keeps Mach-O export linkage separate from Windows probing: %s",
(name, status, basis) => {
const path = "/examples/libnativehelper.dylib";
const result = verifyManagedNativeBoundaries({
...inputWithImportScope(path, {
char_set: "not-specified",
call_convention: "stdcall",
no_mangle: false,
}),
native_observations: [nativeEvidenceWithExports([name], path)],
}).pinvoke_imports[0];
expect(result).toMatchObject({ status, basis });
if (status === "verified")
expect(result?.matched_native).toMatchObject({
name,
source: "macho-export",
});
else
expect(result).toMatchObject({
confidence: "unknown",
matched_native: null,
candidates: [],
});
},
);
it("keeps empty identities unknown and compares suffix-only identities as data", () => {
for (const scope of [null, "", " "]) {
const result = verifyManagedNativeBoundaries({