Files
reverse-engineering-anything/src/domain/analysisView/goBinaryView.ts
T
Kaoru0822andClaude Opus 5.5 b928963675 feat(native): inspect a universal Mach-O slice in Go and Rust metadata readers (#6219)
* feat(native): inspect a universal Mach-O slice in Go and Rust metadata readers

inspect_go_binary and inspect_rust_binary refused universal Mach-O files and
offered no slice selection, so hosts without lipo could not inspect them.

Both now accept architecture (x86, x86_64, arm, arm64). One selector reads
REA's own FAT table, validates the slice range, alignment and header CPU,
and returns the thin image with its macho-selected-image-v1 identity. The
universal file stays the artifact; results add selected_slice and keep
every source range a file offset. Missing, absent, duplicate or misplaced
selections fail on architecture and list the slices present. Subject
format mach-o-universal now has one owner shared by the services and the
retained Go/Rust views.

Fixes #4102

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(native): reject damaged universal files and self-contradicting slice reports

A FAT table whose slice could not be read was treated as a non-universal
file, so callers were told to omit architecture. Its reason now fails on
path. Go and Rust reports with a selected slice must be Mach-O and name the
slice's CPU family, through one shared agreement check. The CLI
architecture flag is described in help.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:18:35 -07:00

301 lines
9.7 KiB
TypeScript

import { metadataReportSubjectFormat } from "../apple/machoTargetSelection.js";
import { z } from "zod";
import type { AnalysisError } from "../analysisErrorBase.js";
import {
goArchitectureForSubject,
goBinarySchema,
goBuildInfoSchema,
goModuleMetadataSchema,
goModuleSchema,
type GoBinary,
} from "../go/goBinary.js";
import { isImmutableJsonSnapshot } from "../immutableJson.js";
import { err, ok, type Result } from "../result.js";
import type {
AnalysisViewParent,
AnalysisViewRequest,
UnsignedAnalysisView,
} from "./analysisView.js";
import {
boundedViewMessages,
completeWithinViewCoverage,
pageViewCoverage,
} from "./analysisViewCoverage.js";
import { analysisViewInputError } from "./analysisViewErrors.js";
import { parseAnalysisViewResult } from "./analysisViewParent.js";
import { selectViewRows } from "./selectViewRows.js";
const indexSchema = z.number().int().nonnegative();
const count = z.number().int().nonnegative();
const settingSchema = goModuleMetadataSchema.shape.settings.element;
const page = {
kind: z.literal("go"),
indices: z.array(indexSchema).optional(),
offset: indexSchema.default(0),
limit: z.number().int().positive().default(64),
};
/** Exact filters intersect; empty arrays select none. Offsets count matching rows. */
export const goBinaryViewRequestSchema = z.discriminatedUnion("collection", [
z.strictObject({
...page,
collection: z.literal("dependencies"),
paths: z.array(z.string()).optional(),
fields: z.array(goModuleSchema.keyof()).optional(),
}),
z.strictObject({
...page,
collection: z.literal("settings"),
keys: z.array(z.string()).optional(),
fields: z.array(settingSchema.keyof()).optional(),
}),
]);
export const goBinaryViewPageSchema = z.discriminatedUnion("collection", [
z.strictObject({
collection: z.literal("dependencies"),
rows: z.array(goModuleSchema.partial().extend({ index: indexSchema })),
}),
z.strictObject({
collection: z.literal("settings"),
rows: z.array(settingSchema.partial().extend({ index: indexSchema })),
}),
]);
export const goBinaryViewContextSchema = z.strictObject({
source_coverage: z.strictObject({
build_info_available: z.boolean(),
module_parse_complete: z.boolean().nullable(),
runtime_dependencies: z.literal("unknown"),
}),
});
export const goBinarySummarySchema = z.strictObject({
bytes: count,
format: goBinarySchema.shape.format,
architecture: goBinarySchema.shape.architecture,
bits: goBinarySchema.shape.bits,
byte_order: goBinarySchema.shape.byte_order,
selected_slice: goBinarySchema.shape.selected_slice,
build_info: goBuildInfoSchema
.omit({
go_version_bytes_base64: true,
module_text: true,
module_bytes_base64: true,
module: true,
})
.extend({
module: goModuleMetadataSchema
.omit({
dependencies: true,
settings: true,
unparsed_lines: true,
unparsed_line_bytes_base64: true,
})
.extend({
available_counts: z.strictObject({
dependencies: count,
settings: count,
unparsed_lines: count,
unparsed_line_bytes_base64: count,
}),
}),
})
.nullable(),
});
// Reuse only authenticated immutable report facts, for their Evidence lifetime.
// Inline mutable reports are validated each time; this never caches live files.
const checkedReports = new WeakMap<object, GoBinary>();
const parseReport = (
parent: AnalysisViewParent,
): Result<GoBinary, AnalysisError> => {
const key = parent.normalizedResult;
const reusable =
parent.sealedResult === true &&
typeof key === "object" &&
key !== null &&
isImmutableJsonSnapshot(key);
const existing = reusable ? checkedReports.get(key) : undefined;
if (existing !== undefined) return ok(existing);
const parsed = parseAnalysisViewResult(goBinarySchema, parent);
if (!parsed.ok) return parsed;
// No schema transforms/coercions: keep the validated owner's frozen value.
const report = reusable ? (key as GoBinary) : parsed.value;
if (reusable) checkedReports.set(key, report);
return ok(report);
};
/** Query the existing metadata owner's facts; never reopen or reinterpret a file. */
export const projectGoBinaryView = (
parent: AnalysisViewParent,
view: AnalysisViewRequest,
): Result<UnsignedAnalysisView, AnalysisError> => {
if (view.kind !== "summary" && view.kind !== "go")
return err(
analysisViewInputError([
{
path: ["view"],
reason: "invalid_value",
message: "Go build Evidence supports summary and go views.",
},
]),
);
const parsed = parseReport(parent);
if (!parsed.ok) return parsed;
const report = parsed.value;
if (
parent.subject?.local_path !== report.artifact.path ||
parent.subject.digest.sha256 !== report.artifact.sha256 ||
parent.subject.format !== metadataReportSubjectFormat(report) ||
parent.subject.architecture !==
(goArchitectureForSubject(report.architecture) ?? null)
)
return err(
analysisViewInputError([
{
path: ["source", "subject"],
reason: "invalid_value",
message:
"Go parent subject must identify the report's exact artifact.",
},
]),
);
const info = report.build_info;
const runtimeUnknown =
"Embedded build records do not establish complete runtime dependencies or target behavior.";
const unknowns = [
runtimeUnknown,
...(info === null
? [
"Build information is unavailable; language and dependencies remain unknown.",
]
: [
...(info.module.complete
? []
: [
"Some module records were not parsed; consult the complete parent Evidence for original lines and bytes.",
]),
...(info.module.main === null
? [
"No main module was decoded; embedded dependency coverage remains unknown.",
]
: []),
]),
];
const shared = {
parent_evidence_id: parent.evidenceId,
parent_operation: "inspect_go_binary" as const,
parent_digest: parent.evidenceId.slice(3),
artifact: { path: report.artifact.path, sha256: report.artifact.sha256 },
source_coverage: {
build_info_available: info !== null,
module_parse_complete: info?.module.complete ?? null,
runtime_dependencies: "unknown" as const,
},
limitations: boundedViewMessages(
[...parent.limitations, ...report.limitations],
parent.evidenceId,
"limitations",
),
unknowns: boundedViewMessages(unknowns, parent.evidenceId, "unknowns", [
runtimeUnknown,
]),
};
if (view.kind === "summary")
return ok({
kind: "summary",
view,
...shared,
summary: {
bytes: report.artifact.bytes,
format: report.format,
architecture: report.architecture,
bits: report.bits,
byte_order: report.byte_order,
...(report.selected_slice === undefined
? {}
: { selected_slice: report.selected_slice }),
build_info:
info === null
? null
: {
header_offset: info.header_offset,
encoding: info.encoding,
go_version: info.go_version,
version_location: info.version_location,
module_location: info.module_location,
module: {
path: info.module.path,
main: info.module.main,
complete: info.module.complete,
available_counts: {
dependencies: info.module.dependencies.length,
settings: info.module.settings.length,
unparsed_lines: info.module.unparsed_lines.length,
unparsed_line_bytes_base64:
info.module.unparsed_line_bytes_base64.length,
},
},
},
},
coverage: completeWithinViewCoverage(1, 1),
});
const indices =
view.indices === undefined ? undefined : new Set(view.indices);
const fields =
view.fields === undefined ? undefined : new Set<string>(view.fields);
if (view.collection === "dependencies") {
const paths = view.paths === undefined ? undefined : new Set(view.paths);
const selected = selectViewRows(
info?.module.dependencies ?? [],
view,
(row, index) =>
(indices?.has(index) ?? true) && (paths?.has(row.path) ?? true),
(row, index) => ({
index,
...((fields?.has("path") ?? true) && { path: row.path }),
...((fields?.has("version") ?? true) && { version: row.version }),
...((fields?.has("sum") ?? true) && { sum: row.sum }),
...((fields?.has("replacement") ?? true) && {
replacement: row.replacement,
}),
}),
);
return ok({
kind: "go",
view,
...shared,
page: { collection: "dependencies", rows: selected.rows },
coverage: pageViewCoverage(
view.offset,
selected.rows.length,
selected.total,
),
});
}
const keys = view.keys === undefined ? undefined : new Set(view.keys);
const selected = selectViewRows(
info?.module.settings ?? [],
view,
(row, index) =>
(indices?.has(index) ?? true) && (keys?.has(row.key) ?? true),
(row, index) => ({
index,
...((fields?.has("key") ?? true) && { key: row.key }),
...((fields?.has("value") ?? true) && { value: row.value }),
}),
);
return ok({
kind: "go",
view,
...shared,
page: { collection: "settings", rows: selected.rows },
coverage: pageViewCoverage(
view.offset,
selected.rows.length,
selected.total,
),
});
};