mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
99d2d7ad22
Share bounded ELF section-name decoding across native and note readers; retain read-only ranges and coverage reasons when names are unknown. Preserve ELF symbol-version bytes, dyld-bound method uncertainty, partial gRPC-Web trailers, and per-parent model identity checks. Compare GPU signatures as bytes and require a PTX module header. Share selected-path errno classification across EVM and BEAM. Validation: 397 shared-parser tests passed (one existing skip), 5 MCP schema tests passed, 14 EVM error-mapping tests passed, and 23 protobuf CLI/MCP matrix cases passed across focused runs. Build, typecheck/lint, docs, formatting, and 2134-file discovery passed. Full EVM provider workflow remains locally unverified on Darwin arm64; format controls are synthetic. Fixes #6738 Fixes #6737 Fixes #6745 Fixes #6740 Fixes #6749 Fixes #6747 Fixes #6739
356 lines
12 KiB
TypeScript
356 lines
12 KiB
TypeScript
import { readStableArtifact } from "../artifacts/readStableArtifact.js";
|
|
import {
|
|
artifactFilePrimaryFailure,
|
|
artifactFileCancellationCause,
|
|
assertArtifactReadActive,
|
|
bindArtifactFileCleanup,
|
|
} from "../artifacts/ArtifactFileOperation.js";
|
|
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
|
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
|
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
|
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
|
import type { ExecutionOptions } from "../application/AnalysisProvider.js";
|
|
import type { RustBinaryPort } from "../application/rust/RustBinaryPort.js";
|
|
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
|
import {
|
|
AnalysisAccessDeniedError,
|
|
AnalysisArtifactChangedError,
|
|
AnalysisCancelledError,
|
|
AnalysisInputError,
|
|
AnalysisOutputError,
|
|
AnalysisResourceConstraintError,
|
|
AnalysisUnsupportedTargetError,
|
|
} from "../domain/analysisErrorCore.js";
|
|
import {
|
|
RUST_AUDITABLE_FORMAT_VERSIONS,
|
|
rustBinarySchema,
|
|
type InspectRustBinaryInput,
|
|
type RustBinary,
|
|
} from "../domain/rust/rustBinary.js";
|
|
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
|
|
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
|
import { err, ok, type Result } from "../domain/result.js";
|
|
import {
|
|
readRustBinaryImage,
|
|
type RustBinaryImage,
|
|
} from "./RustBinaryImage.js";
|
|
import { requireSelectedNativeImage } from "../native/metadata/MachoSliceSelection.js";
|
|
import {
|
|
RUST_BINARY_INPUT_BYTES,
|
|
RUST_OUTPUT_BYTES,
|
|
RustAuditableFormatFailure,
|
|
RustAuditableResourceFailure,
|
|
} from "./RustAuditableReader.js";
|
|
|
|
/** Bundled reader identity. Embedded package versions are separate observed facts. */
|
|
export const RUST_BINARY_PROVIDER_IDENTITY = {
|
|
id: "rust-auditable",
|
|
name: "REA Rust dependency reader",
|
|
version: "1",
|
|
} as const;
|
|
const OPERATION = "inspect_rust_binary";
|
|
|
|
/** Stable in-process dependency inspection without subprocesses, temporary files, or a Rust toolchain. */
|
|
export class RustBinaryProvider implements RustBinaryPort {
|
|
readonly identity = RUST_BINARY_PROVIDER_IDENTITY;
|
|
readonly #resources = new ArtifactResourceScope();
|
|
|
|
async close(): Promise<void> {
|
|
try {
|
|
await this.#resources.close();
|
|
} catch (cause: unknown) {
|
|
if (
|
|
!(cause instanceof ArtifactReaderFailure) ||
|
|
cause.cleanup === undefined
|
|
)
|
|
throw cause;
|
|
const error = new ProviderCleanupError(
|
|
RUST_BINARY_PROVIDER_IDENTITY.id,
|
|
cause.cleanup.resources,
|
|
{ reason: cause.cleanup.reason },
|
|
{ cause, operation: OPERATION },
|
|
);
|
|
bindArtifactFileCleanup(cause, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/** Read one dependency section and any inferred rustc commit from the selected file. */
|
|
async inspect(
|
|
input: InspectRustBinaryInput,
|
|
options?: ExecutionOptions,
|
|
): Promise<Result<RustBinary, AnalysisError>> {
|
|
try {
|
|
return await this.#resources.run(async () => {
|
|
const snapshot = await readStableArtifact(
|
|
input.path,
|
|
RUST_BINARY_INPUT_BYTES,
|
|
options?.signal,
|
|
undefined,
|
|
this.#resources,
|
|
);
|
|
assertArtifactReadActive(options?.signal);
|
|
const selected = requireSelectedNativeImage(
|
|
OPERATION,
|
|
input.path,
|
|
snapshot.bytes,
|
|
input.architecture,
|
|
);
|
|
const image = inFile(
|
|
readRustBinaryImage(selected.bytes),
|
|
selected.fileOffset,
|
|
);
|
|
const report = {
|
|
format: image.format,
|
|
architecture: image.architecture,
|
|
bits: image.bits,
|
|
byte_order: image.byte_order,
|
|
auditable_metadata: image.auditable_metadata,
|
|
rustc_commit: image.rustc_commit,
|
|
artifact: {
|
|
path: input.path,
|
|
sha256: snapshot.sha256,
|
|
bytes: snapshot.bytes.length,
|
|
},
|
|
...(selected.selection === null
|
|
? {}
|
|
: { selected_slice: selected.selection }),
|
|
limitations: limitationsFor(image),
|
|
};
|
|
if (
|
|
Buffer.byteLength(JSON.stringify(report), "utf8") > RUST_OUTPUT_BYTES
|
|
)
|
|
throw outputLimit(input.path);
|
|
const parsed = rustBinarySchema.safeParse(report);
|
|
if (!parsed.success)
|
|
throw new AnalysisOutputError(
|
|
OPERATION,
|
|
"Rust reader produced an invalid dependency report.",
|
|
{ cause: parsed.error },
|
|
);
|
|
assertArtifactReadActive(options?.signal);
|
|
return ok(parsed.data);
|
|
});
|
|
} catch (cause: unknown) {
|
|
return err(failureFor(cause, input.path));
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Locate image-relative section and rustc-path offsets in the selected file. */
|
|
const inFile = (image: RustBinaryImage, fileOffset: number): RustBinaryImage =>
|
|
fileOffset === 0
|
|
? image
|
|
: {
|
|
...image,
|
|
auditable_metadata:
|
|
image.auditable_metadata === null
|
|
? null
|
|
: {
|
|
...image.auditable_metadata,
|
|
file_offset: image.auditable_metadata.file_offset + fileOffset,
|
|
},
|
|
rustc_commit:
|
|
image.rustc_commit === null
|
|
? null
|
|
: {
|
|
...image.rustc_commit,
|
|
hashes: image.rustc_commit.hashes.map((entry) => ({
|
|
...entry,
|
|
file_offset: entry.file_offset + fileOffset,
|
|
})),
|
|
},
|
|
};
|
|
|
|
const outputLimit = (path: string) =>
|
|
new AnalysisResourceConstraintError(
|
|
OPERATION,
|
|
"transport",
|
|
`Complete Rust metadata for ${path} exceeds the inline output budget; no partial success is returned.`,
|
|
{ maximum_output_bytes: RUST_OUTPUT_BYTES },
|
|
fixedAllowanceOptions("The Rust complete-report allowance", "report"),
|
|
);
|
|
|
|
const limitationsFor = (
|
|
image: ReturnType<typeof readRustBinaryImage>,
|
|
): string[] => [
|
|
"Embedded dependency metadata is file content. It does not authenticate package names, versions, sources, or the compiler that produced the binary.",
|
|
"Only the .dep-v0 dependency section and read-only compiler path strings are inspected. Functions, types, and runtime behavior are not recovered.",
|
|
"Resource guards bound the selected file to 256 MiB, a dependency section to 1 MiB, inflated JSON to the smaller of 8 MiB and 64 times the section size, and the complete report to 8 MiB. Package and dependency counts are admitted against that output budget before schema cloning, graph objects, or reverse-edge allocation.",
|
|
...(image.section_names_limitation === undefined
|
|
? []
|
|
: [image.section_names_limitation]),
|
|
...(image.auditable_metadata === null &&
|
|
image.section_names_limitation === undefined
|
|
? [
|
|
"No .dep-v0 section was found. Absence of .dep-v0 does not mean the binary has no dependencies.",
|
|
]
|
|
: []),
|
|
...(image.auditable_metadata === null &&
|
|
image.section_names_limitation !== undefined
|
|
? [
|
|
"The presence of .dep-v0 could not be determined because ELF section names are unavailable.",
|
|
]
|
|
: []),
|
|
...unknownFormatLimitation(image.auditable_metadata?.format_version),
|
|
...(image.auditable_metadata?.packages.some((pkg) => pkg.kind === null)
|
|
? [
|
|
"An omitted package kind is left null. The embedded format treats an omitted kind as runtime, and that default was not stored in the section.",
|
|
]
|
|
: []),
|
|
...(image.auditable_metadata !== null &&
|
|
image.auditable_metadata.cyclic_dependency_indices.length > 0
|
|
? [
|
|
"Dependency indices contain a cycle. The cycle is reported and was not traversed.",
|
|
]
|
|
: []),
|
|
...rustcLimitation(image.rustc_commit),
|
|
];
|
|
|
|
const unknownFormatLimitation = (
|
|
format: number | null | undefined,
|
|
): string[] => {
|
|
if (format === null || format === undefined) return [];
|
|
let preceding = 0;
|
|
for (const known of RUST_AUDITABLE_FORMAT_VERSIONS) {
|
|
if (known === format) return [];
|
|
if (known < format) preceding = known;
|
|
}
|
|
return [
|
|
`cargo-auditable format revision ${format} is unknown. Its schema directs readers to interpret it using the highest known preceding revision (${preceding}); this specific revision has not been independently verified.`,
|
|
];
|
|
};
|
|
|
|
const rustcLimitation = (
|
|
commit: ReturnType<typeof readRustBinaryImage>["rustc_commit"],
|
|
): string[] => {
|
|
if (commit === null)
|
|
return [
|
|
"No /rustc/<40-hex>/ path was found in read-only data, so a rustc commit hash is unknown.",
|
|
];
|
|
if (commit.hashes.length === 1)
|
|
return [
|
|
"The rustc commit hash is inferred from a /rustc/<40-hex>/ path in read-only data. It does not by itself prove which compiler produced the binary.",
|
|
];
|
|
return [
|
|
"Read-only data contains more than one /rustc/<40-hex>/ path, so a single rustc commit is unknown.",
|
|
];
|
|
};
|
|
|
|
const failureForPrimary = (cause: unknown, path: string): AnalysisError => {
|
|
if (cause instanceof AnalysisError) return cause;
|
|
if (cause instanceof RustAuditableResourceFailure)
|
|
return new AnalysisResourceConstraintError(
|
|
OPERATION,
|
|
cause.constraint.resource,
|
|
`Selected artifact ${path}: ${cause.message}`,
|
|
cause.constraint.reported_limits,
|
|
{
|
|
cause,
|
|
...fixedAllowanceOptions(
|
|
cause.constraint.resource === "capacity"
|
|
? "The rustc commit-path inventory"
|
|
: cause.constraint.resource === "transport"
|
|
? "The Rust complete-report allowance"
|
|
: "This Rust inspection allowance",
|
|
cause.constraint.resource === "capacity"
|
|
? "count"
|
|
: cause.constraint.resource === "transport"
|
|
? "report"
|
|
: "parser",
|
|
),
|
|
},
|
|
);
|
|
if (cause instanceof RustAuditableFormatFailure)
|
|
return cause.kind === "unsupported"
|
|
? new AnalysisUnsupportedTargetError(OPERATION, path, cause.message, {
|
|
cause,
|
|
})
|
|
: new AnalysisInputError(OPERATION, { cause }, [
|
|
{
|
|
path: ["path"],
|
|
reason: "invalid_format",
|
|
message: `Selected artifact ${path}: ${cause.message}`,
|
|
},
|
|
]);
|
|
return artifactFailure(cause, path);
|
|
};
|
|
|
|
const failureFor = (cause: unknown, path: string): AnalysisError => {
|
|
const primary = failureForPrimary(artifactFilePrimaryFailure(cause), path);
|
|
if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined)
|
|
return primary;
|
|
const cleanup = new ProviderCleanupError(
|
|
RUST_BINARY_PROVIDER_IDENTITY.id,
|
|
cause.cleanup.resources,
|
|
{ reason: cause.cleanup.reason },
|
|
{ cause, operation: OPERATION },
|
|
);
|
|
const error = analysisErrorWithCleanupFailure(primary, cleanup, OPERATION);
|
|
bindArtifactFileCleanup(cause, error);
|
|
return error;
|
|
};
|
|
|
|
const artifactFailure = (cause: unknown, path: string): AnalysisError => {
|
|
if (cause instanceof ArtifactReaderFailure)
|
|
return artifactReaderFailure(cause, path);
|
|
if (cause instanceof Error && "code" in cause) {
|
|
if (cause.code === "EACCES" || cause.code === "EPERM")
|
|
return new AnalysisAccessDeniedError(OPERATION, path, cause.code, {
|
|
cause,
|
|
});
|
|
if (
|
|
["ENOENT", "ENOTDIR", "ELOOP", "ENAMETOOLONG"].includes(
|
|
String(cause.code),
|
|
)
|
|
)
|
|
return new AnalysisInputError(OPERATION, { cause }, [
|
|
{
|
|
path: ["path"],
|
|
reason: "invalid_value",
|
|
message: `Selected artifact could not be read (${String(cause.code)}): ${path}.`,
|
|
},
|
|
]);
|
|
}
|
|
return new ProviderAdapterError(RUST_BINARY_PROVIDER_IDENTITY.id, OPERATION, {
|
|
cause,
|
|
diagnostics: {
|
|
path,
|
|
reason: cause instanceof Error ? cause.message : String(cause),
|
|
},
|
|
});
|
|
};
|
|
|
|
const artifactReaderFailure = (
|
|
cause: ArtifactReaderFailure,
|
|
path: string,
|
|
): AnalysisError => {
|
|
if (cause.reason === "integrity")
|
|
return new AnalysisArtifactChangedError(OPERATION, path, cause.message, {
|
|
cause,
|
|
});
|
|
if (cause.reason === "cancelled")
|
|
return new AnalysisCancelledError(OPERATION, {
|
|
cause: artifactFileCancellationCause(cause),
|
|
});
|
|
if (cause.reason === "limit")
|
|
return new AnalysisResourceConstraintError(
|
|
OPERATION,
|
|
"file-size",
|
|
cause.message,
|
|
{ maximum_input_bytes: RUST_BINARY_INPUT_BYTES },
|
|
{
|
|
cause,
|
|
...fixedAllowanceOptions("the fixed Rust input allowance", "input"),
|
|
},
|
|
);
|
|
if (cause.reason === "path")
|
|
return new AnalysisInputError(OPERATION, { cause }, [
|
|
{ path: ["path"], reason: "invalid_format", message: cause.message },
|
|
]);
|
|
return new ProviderAdapterError(RUST_BINARY_PROVIDER_IDENTITY.id, OPERATION, {
|
|
cause,
|
|
diagnostics: { path, reason: cause.message },
|
|
});
|
|
};
|