mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 00:36:35 +00:00
* refactor(macho): extract non-throwing container and slice load-command readers Split readMachoImage into readMachoContainer, which classifies a file as thin, FAT or not Mach-O and keeps FAT range issues per slice, and readSliceLoadCommands, which reads one slice header and frames its load commands, returning every command framed before a failure. readMachoImage keeps its strict policy on top of them: commands framed before a framing defect still decode first, an unsupported FAT slice still yields to a later malformed one, and the first FAT range issue is still reported. A characterization test pins that precedence for inputs with several defects; it passes against both the previous and the new implementation. Part of #4306. * feat(macho): report per-slice encryption status in inspect_macho inspect_macho now reports the LC_ENCRYPTION_INFO(_64) state of every slice in a new `encryption` collection, read in process from load commands without reading code bytes. - Each slice is encrypted, not-encrypted, absent, malformed or unknown. Absent requires a completed load-command walk; malformed and unknown carry a reason and keep commands read before the failure. - The named range is end-exclusive in slice and input-file coordinates and maps to file-backed segment fragments, VM addresses and the overlapping nonempty sections. - A ciphertext range adds a limitation naming its input-file bytes. - The in-process read and the native tools are bracketed by one regular file descriptor and stat identity checks; a change reports artifact_changed. A missing or inaccessible target keeps the tool facts and reports encryption as non-exhaustive, never absent. - Retained Evidence recorded before this field is parsed by the owning schema and reports encryption as unknown in the macho view and in managed/native verification. Fixes #4306 * fix(macho): keep slice facts and failures under large tables and close errors - Frame load commands through a visitor instead of retaining every command, restoring the pre-refactor memory profile (2M eight-byte commands: peak heap 370 MiB -> 0.9 MiB). - Confirm a large FAT container from a valid slice's Mach-O header so a sibling's range fault stays per slice instead of hiding healthy slices. - Name only sections whose bytes the range and their segment both cover. - Throw inspection failures inside the owned descriptor callback so a close failure keeps them as its primary error. * fix(macho): map encrypted ranges only within segment VM extents mapRange gave every file-backed segment fragment VM addresses, even for a segment with no VM size, whose file bytes dyld never maps, and past vmsize when a segment declared more file bytes than memory. Read vmsize with the segment. A segment with no VM size keeps its fragment with null VM addresses. A file size beyond the VM size, or a VM range past the address space, makes the mapping unknown with a reason while the slice keeps its encryption status. The docs also claimed the encryption read never reads code bytes. Like every Mach-O container read, a 0xcafebabe input of at most 16 MiB is read whole to rule out a JVM class file; say so. * fix(macho): read encryption through the admitted descriptor facts Main's withArtifactRegularFile now hands callers the admitted handle and its initial stats. Use those facts instead of re-stating the handle, and pass the legacy Evidence payload straight to createEvidence. * fix(macho): retain a finished report when the target close fails The file owner keeps the completed callback value when the descriptor close fails or cancellation arrives during close. inspect_macho now validates that value and retains the report as the error's partial observation, as inspect_signature does, instead of returning only the cleanup or cancellation error. * test(native): supply readEncryption in merged dyld refusal cases main's new dyld refusal cases build NativeMachoInspectionContext without the encryption reader this branch requires. * test(views): bind the Mach-O encryption page to its facet main's facet-ownership case builds inspect_macho Evidence without the encryption coverage this branch requires; the fixture now carries one slice row and the loop checks the encryption page like the other facets. * fix(macho): bound encryption commands and range mapping across slices A slice retains at most two encryption load commands, since a second one already makes it malformed; the reason keeps the full count. Range mapping in every slice shares one budget of segment and section records, charged from each segment command's capacity before it is decoded. A slice past the budget keeps its encryption status and reports an unknown mapping. * style(macho): format encryption reader and binding test * fix(macho): stop retaining segments past the mapping budget Segment commands are collected during framing only while their mapping records still fit in the budget shared by all slices, and the budget is charged only when the slice maps its range. * docs(macho): clarify retained mapping allowance * fix(ci): own the Mach-O encryption test in Apple scope --------- Co-authored-by: t <t@t> Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
294 lines
9.6 KiB
TypeScript
294 lines
9.6 KiB
TypeScript
import { z } from "zod";
|
|
import { bundleResourceRootSchema } from "../apple/bundleResourceRoot.js";
|
|
import { jsonRecordSchema } from "../jsonRecord.js";
|
|
import { jsonValueSchema } from "../jsonValue.js";
|
|
import { machoSliceEncryptionSchema } from "../apple/machoEncryption.js";
|
|
|
|
const hexAddress = z.string().regex(/^0x[a-fA-F0-9]+$/u);
|
|
const bytesHex = z.string().regex(/^(?:[0-9a-f]{2})*$/u);
|
|
const coverage = <Schema extends z.ZodType>(item: Schema) =>
|
|
z.object({
|
|
items: z.array(item),
|
|
total: z.int().min(0).nullable(),
|
|
exhaustive: z.boolean(),
|
|
limitations: z.array(z.string()),
|
|
});
|
|
|
|
const nativeCommandInvocationSchema = z.object({
|
|
tool: z.string().min(1),
|
|
command: z.array(z.string()),
|
|
tool_version: z.string().nullable(),
|
|
version_reason: z.string().nullable(),
|
|
executable_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
exit: z.object({
|
|
code: z.int().nullable(),
|
|
signal: z.string().nullable(),
|
|
}),
|
|
stdout_bytes: z.int().min(0),
|
|
stderr_bytes: z.int().min(0),
|
|
});
|
|
|
|
const architectureSchema = z.object({
|
|
name: z.string(),
|
|
hidden: z.boolean().exactOptional(),
|
|
cpu_type: z.string().nullable(),
|
|
cpu_subtype: z.string().nullable(),
|
|
file_offset: z
|
|
.int()
|
|
.min(0)
|
|
.nullable()
|
|
.describe("Offset from the start of the input Mach-O container."),
|
|
size: z.int().min(0).nullable(),
|
|
alignment: z.int().min(0).nullable(),
|
|
});
|
|
const permissionSchema = z.object({
|
|
read: z.boolean().nullable(),
|
|
write: z.boolean().nullable(),
|
|
execute: z.boolean().nullable(),
|
|
raw: z.string().nullable(),
|
|
});
|
|
const sectionSchema = z.object({
|
|
segment: z.string().nullable(),
|
|
segment_bytes_hex: bytesHex
|
|
.optional()
|
|
.describe(
|
|
"Original segment-name bytes before the first NUL; omitted in older Evidence.",
|
|
),
|
|
name: z.string().nullable(),
|
|
name_bytes_hex: bytesHex
|
|
.optional()
|
|
.describe(
|
|
"Original section-name bytes before the first NUL; omitted in older Evidence.",
|
|
),
|
|
address: hexAddress.nullable(),
|
|
size: z.int().min(0).nullable(),
|
|
size_hex: hexAddress
|
|
.optional()
|
|
.describe(
|
|
"Exact section size when it cannot be represented as a safe JSON integer.",
|
|
),
|
|
file_offset: z
|
|
.int()
|
|
.min(0)
|
|
.nullable()
|
|
.describe("Offset from the start of the selected Mach-O slice."),
|
|
alignment: z.int().min(0).nullable(),
|
|
alignment_exponent: z
|
|
.int()
|
|
.min(0)
|
|
.optional()
|
|
.describe(
|
|
"Exact binary alignment exponent when the corresponding byte alignment is not a safe JSON integer.",
|
|
),
|
|
flags: z.array(z.string()),
|
|
});
|
|
const segmentSchema = z.object({
|
|
name: z.string().nullable(),
|
|
name_bytes_hex: bytesHex
|
|
.optional()
|
|
.describe(
|
|
"Original segment-name bytes before the first NUL; omitted in older Evidence.",
|
|
),
|
|
vm_address: hexAddress.nullable(),
|
|
vm_size: z.int().min(0).nullable(),
|
|
vm_size_hex: hexAddress
|
|
.optional()
|
|
.describe(
|
|
"Exact VM size when it cannot be represented as a safe JSON integer.",
|
|
),
|
|
file_offset: z
|
|
.int()
|
|
.min(0)
|
|
.nullable()
|
|
.describe("Offset from the start of the selected Mach-O slice."),
|
|
file_offset_hex: hexAddress
|
|
.optional()
|
|
.describe(
|
|
"Exact slice-relative file offset when it cannot be represented as a safe JSON integer.",
|
|
),
|
|
file_size: z.int().min(0).nullable(),
|
|
file_size_hex: hexAddress
|
|
.optional()
|
|
.describe(
|
|
"Exact file size when it cannot be represented as a safe JSON integer.",
|
|
),
|
|
maximum_permissions: permissionSchema,
|
|
initial_permissions: permissionSchema,
|
|
sections: coverage(sectionSchema),
|
|
});
|
|
/** Derived from `name` by the shared Mach-O symbol-name owner. */
|
|
const runtimeNameSchema = z
|
|
.string()
|
|
.nullable()
|
|
.default(null)
|
|
.describe(
|
|
"Symbol-table `name` without the one leading underscore Darwin adds to language-level symbols (`_main` is `main`, `__ZN3rea3addEii` is `_ZN3rea3addEii`), as LLDB and dlsym name it and observe_native_calls function breakpoints accept; a name without that underscore, such as `-[Class selector]`, is unchanged. Null when `name` is null, nothing remains, or the Evidence predates this field.",
|
|
);
|
|
const symbolSchema = z.object({
|
|
/** Exact Mach-O symbol-table name, the source fact. */
|
|
name: z.string(),
|
|
runtime_name: runtimeNameSchema,
|
|
address: hexAddress.nullable(),
|
|
weak: z.boolean().nullable(),
|
|
reexport: z.boolean().nullable(),
|
|
source: z.string().nullable(),
|
|
});
|
|
const importSymbolSchema = z.object({
|
|
/** Resolved symbol identity; null when the textual row has multiple parses. */
|
|
name: z.string().nullable(),
|
|
runtime_name: runtimeNameSchema,
|
|
/** Exact import text retained when a chained suffix makes identity ambiguous. */
|
|
raw_name: z.string().nullable().default(null),
|
|
name_status: z.enum(["exact", "ambiguous"]).nullable().default(null),
|
|
/** Addend is unknown when only the ambiguous text expression is available. */
|
|
addend: hexAddress.nullable().default(null),
|
|
address: hexAddress.nullable(),
|
|
weak: z.boolean().nullable(),
|
|
reexport: z.boolean().nullable(),
|
|
source: z.string().nullable(),
|
|
});
|
|
const dependencySchema = z.object({
|
|
path: z.string().nullable(),
|
|
path_bytes_hex: bytesHex
|
|
.optional()
|
|
.describe(
|
|
"Original load-command path bytes before the first NUL; omitted in older Evidence.",
|
|
),
|
|
kind: z.string(),
|
|
current_version: z.string().nullable(),
|
|
compatibility_version: z.string().nullable(),
|
|
});
|
|
const loadCommandSchema = z.object({
|
|
index: z.int().min(0),
|
|
kind: z.string(),
|
|
fields: jsonRecordSchema(z.union([z.string(), z.number(), z.null()])),
|
|
});
|
|
const buildMetadataSchema = z.object({
|
|
platform: z.string().nullable(),
|
|
minimum_os: z.string().nullable(),
|
|
sdk: z.string().nullable(),
|
|
tools: z.array(z.object({ name: z.string(), version: z.string() })),
|
|
});
|
|
|
|
export const inspectMachoSchema = z.object({
|
|
format: z.enum(["mach-o", "mach-o-universal"]),
|
|
/** Exact lipo slice selected for inspection and its offset in the input container. */
|
|
selected_architecture: z.object({
|
|
name: z
|
|
.string()
|
|
.nullable()
|
|
.describe(
|
|
"Exact architecture name selected from lipo output; null when selection is unknown.",
|
|
),
|
|
container_file_offset: z
|
|
.int()
|
|
.min(0)
|
|
.nullable()
|
|
.describe(
|
|
"Offset of the selected slice from the start of the input Mach-O container; null when unknown.",
|
|
),
|
|
}),
|
|
endian: z.enum(["little", "big"]).nullable(),
|
|
word_size: z.union([z.literal(32), z.literal(64)]).nullable(),
|
|
file_type: z.string().nullable(),
|
|
flags: z.array(z.string()),
|
|
uuid: z.string().nullable(),
|
|
entrypoints: coverage(
|
|
z.object({
|
|
file_offset: z
|
|
.int()
|
|
.min(0)
|
|
.describe(
|
|
"LC_MAIN entry offset from the start of the selected Mach-O slice.",
|
|
),
|
|
}),
|
|
),
|
|
architectures: coverage(architectureSchema),
|
|
build_metadata: coverage(buildMetadataSchema),
|
|
load_commands: coverage(loadCommandSchema),
|
|
dependencies: coverage(dependencySchema),
|
|
imports: coverage(importSymbolSchema),
|
|
exports: coverage(symbolSchema),
|
|
segments: coverage(segmentSchema),
|
|
encryption: coverage(machoSliceEncryptionSchema).describe(
|
|
"LC_ENCRYPTION_INFO(_64) status of every slice, read from load commands only.",
|
|
),
|
|
provenance: z.array(nativeCommandInvocationSchema),
|
|
limitations: z.array(z.string()),
|
|
});
|
|
|
|
/**
|
|
* Retained inspect_macho Evidence, including results recorded before
|
|
* `encryption` was reported. Not recorded is unknown, never evidence that no
|
|
* slice is encrypted.
|
|
*/
|
|
export const retainedInspectMachoSchema = inspectMachoSchema
|
|
.extend({ encryption: inspectMachoSchema.shape.encryption.optional() })
|
|
.transform(({ encryption, ...report }) => ({
|
|
...report,
|
|
encryption: encryption ?? {
|
|
items: [],
|
|
total: null,
|
|
exhaustive: false,
|
|
limitations: [
|
|
"This inspect_macho Evidence predates encryption reporting; slice encryption status is unknown.",
|
|
],
|
|
},
|
|
}));
|
|
|
|
export const inspectSignatureSchema = z.object({
|
|
signed: z.boolean(),
|
|
identifier: z.string().nullable(),
|
|
team_identifier: z.string().nullable(),
|
|
format: z.string().nullable(),
|
|
cdhashes: z.array(z.string()),
|
|
hash_algorithms: z.array(z.string()),
|
|
authorities: z.array(z.string()),
|
|
designated_requirement: z.string().nullable(),
|
|
entitlements: jsonValueSchema.nullable(),
|
|
timestamp: z.string().nullable(),
|
|
hardened_runtime: z.boolean().nullable(),
|
|
provenance: z.array(nativeCommandInvocationSchema),
|
|
limitations: z.array(z.string()),
|
|
});
|
|
|
|
export const inspectPlistSchema = z.object({
|
|
format: z.enum(["xml", "binary", "json", "unknown"]),
|
|
value: jsonValueSchema,
|
|
bundle: z.object({
|
|
identifier: z.string().nullable(),
|
|
executable: z.string().nullable(),
|
|
name: z.string().nullable(),
|
|
version: z.string().nullable(),
|
|
short_version: z.string().nullable(),
|
|
}),
|
|
source_path: z.string(),
|
|
resource_root: bundleResourceRootSchema.optional(),
|
|
provenance: z.array(nativeCommandInvocationSchema),
|
|
limitations: z.array(z.string()),
|
|
});
|
|
|
|
export const listArchitecturesSchema = z.object({
|
|
architectures: coverage(architectureSchema),
|
|
provenance: z.array(nativeCommandInvocationSchema),
|
|
limitations: z.array(z.string()),
|
|
});
|
|
|
|
export const demangleSwiftSchema = z.object({
|
|
symbols: z.array(
|
|
z.object({
|
|
input: z.string(),
|
|
output: z.string(),
|
|
status: z.enum(["demangled", "unchanged", "invalid"]),
|
|
}),
|
|
),
|
|
provenance: z.array(nativeCommandInvocationSchema),
|
|
limitations: z.array(z.string()),
|
|
});
|
|
|
|
export type NativeCommandInvocation = z.infer<
|
|
typeof nativeCommandInvocationSchema
|
|
>;
|
|
export type InspectMacho = z.infer<typeof inspectMachoSchema>;
|
|
export type InspectSignature = z.infer<typeof inspectSignatureSchema>;
|