Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d22729b85 | ||
|
|
ae38e0c73f |
No files matched your search
@@ -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({
|
||||
|
||||
Reference in new issue
Block a user