mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
fix(native): classify observed dSYM inspection refusal
This commit is contained in:
@@ -409,6 +409,12 @@ A conversion refusal includes plutil's diagnostic from either output stream.
|
||||
`details.captured_output` retains bounded stdout/stderr excerpts, observed byte
|
||||
totals and exit status. `truncated: true` means those excerpts are incomplete.
|
||||
|
||||
A dSYM companion that `dyld_info` refuses for an unbacked text section reports
|
||||
`unsupported_target`, retaining the exact diagnostic and directing inspection
|
||||
to the linked Mach-O. This classification requires both the recognized native
|
||||
refusal and an independently parsed dSYM Mach header; other tool failures keep
|
||||
their failure outcome.
|
||||
|
||||
`inspect_macho.selected_architecture` identifies the exact selected lipo slice
|
||||
and its `container_file_offset` in the input file. A thin Mach-O has a known
|
||||
offset of zero; an unknown universal-slice offset stays null. The Evidence
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AnalysisCapturedOutput } from "../domain/analysisErrorBase.js";
|
||||
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
|
||||
import type { NativeCommandCapture } from "./CommandRunner.js";
|
||||
import { withoutEchoedPathHeader } from "./parsers/echoedPath.js";
|
||||
@@ -20,6 +21,37 @@ export const isMissingUuidDyldFailure = (
|
||||
MISSING_UUID_DIAGNOSTIC,
|
||||
);
|
||||
|
||||
/** The narrowly observed dyld refusal for an unbacked dSYM text section. */
|
||||
export const isUnbackedTextDyldFailure = (
|
||||
capture: NativeCommandCapture,
|
||||
): boolean =>
|
||||
capture.tool === "dyld_info" &&
|
||||
capture.exitCode !== null &&
|
||||
capture.exitCode !== 0 &&
|
||||
capture.signal === null &&
|
||||
[capture.stdout, capture.stderr].some((output) =>
|
||||
withoutEchoedPathHeader(output, capture.arguments.at(-1))
|
||||
.split(/\r?\n/u)
|
||||
.some(
|
||||
(line) =>
|
||||
line.trim().replace(/^(?:dyld_info:\s*)/u, "") ===
|
||||
"section '__TEXT/__text' has offset=0 but is not a zero-fill section type",
|
||||
),
|
||||
);
|
||||
|
||||
/** Complete captures come from the settled native command owner. */
|
||||
export const nativeMachoCapturedOutput = (
|
||||
capture: NativeCommandCapture,
|
||||
): AnalysisCapturedOutput => ({
|
||||
stdout: capture.stdout,
|
||||
stderr: capture.stderr,
|
||||
truncated: false,
|
||||
stdout_bytes: capture.stdoutBytes,
|
||||
stderr_bytes: capture.stderrBytes,
|
||||
exit_code: capture.exitCode,
|
||||
signal: capture.signal,
|
||||
});
|
||||
|
||||
/** Keep the exact target diagnostic beside the incomplete collection. */
|
||||
export const missingUuidLimitation = (mode: "imports" | "exports"): string =>
|
||||
mode === "imports"
|
||||
@@ -34,15 +66,7 @@ export const unusableNativeCommandOutput = (
|
||||
"inspect_macho",
|
||||
`${capture.tool} did not produce a usable inspection result`,
|
||||
{
|
||||
capturedOutput: {
|
||||
stdout: capture.stdout,
|
||||
stderr: capture.stderr,
|
||||
truncated: false,
|
||||
stdout_bytes: capture.stdoutBytes,
|
||||
stderr_bytes: capture.stderrBytes,
|
||||
exit_code: capture.exitCode,
|
||||
signal: capture.signal,
|
||||
},
|
||||
capturedOutput: nativeMachoCapturedOutput(capture),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -54,14 +78,6 @@ export const conflictingUuidCommandEvidence = (
|
||||
"inspect_macho",
|
||||
"dyld_info reported a missing LC_UUID despite an observed LC_UUID load command",
|
||||
{
|
||||
capturedOutput: {
|
||||
stdout: command.stdout,
|
||||
stderr: command.stderr,
|
||||
truncated: false,
|
||||
stdout_bytes: command.stdoutBytes,
|
||||
stderr_bytes: command.stderrBytes,
|
||||
exit_code: command.exitCode,
|
||||
signal: command.signal,
|
||||
},
|
||||
capturedOutput: nativeMachoCapturedOutput(command),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -403,3 +403,51 @@ describe("Mach-O slice command scheduling", () => {
|
||||
expect(result.error).toMatchObject({ reason: "nm failed" });
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["10", "DSYM", "2"])(
|
||||
"classifies the unbacked-text dyld refusal using observed file type %s",
|
||||
async (fileType) => {
|
||||
const diagnostic =
|
||||
"section '__TEXT/__text' has offset=0 but is not a zero-fill section type\n";
|
||||
const runner = new NativeFixtureRunner({
|
||||
otool: [
|
||||
"Mach header",
|
||||
"magic cputype cpusubtype caps filetype ncmds sizeofcmds flags",
|
||||
`0xfeedfacf 16777228 0 0x00 ${fileType} 0 0 0x0`,
|
||||
].join("\n"),
|
||||
});
|
||||
const result = await inspectNativeMacho({
|
||||
target: nativeMachoTarget("/fixture/debug-companion"),
|
||||
run: async (tool, args, _signal, acceptNonZeroIf) => {
|
||||
const captured = await runner.run(tool, args);
|
||||
if (!captured.ok || tool !== "dyld_info") return captured;
|
||||
const refused = {
|
||||
...captured.value,
|
||||
stdout: diagnostic,
|
||||
stdoutBytes: Buffer.byteLength(diagnostic),
|
||||
exitCode: 1,
|
||||
};
|
||||
if (acceptNonZeroIf?.(refused)) return ok(refused);
|
||||
return err(
|
||||
new AnalysisOutputError("inspect_macho", "unaccepted native failure"),
|
||||
);
|
||||
},
|
||||
invocation: () => {
|
||||
throw new Error(
|
||||
"Refused target does not produce inspection provenance",
|
||||
);
|
||||
},
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error._tag).toBe(
|
||||
fileType === "2"
|
||||
? "AnalysisOutputError"
|
||||
: "AnalysisUnsupportedTargetError",
|
||||
);
|
||||
expect(result.error.capturedOutput).toMatchObject({
|
||||
stdout: diagnostic,
|
||||
exit_code: 1,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import type { EvidenceLocation } from "../domain/evidence.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
AnalysisOutputError,
|
||||
AnalysisUnsupportedTargetError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
inspectMachoSchema,
|
||||
type NativeCommandInvocation,
|
||||
@@ -22,6 +25,8 @@ import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
import {
|
||||
conflictingUuidCommandEvidence,
|
||||
isMissingUuidDyldFailure,
|
||||
isUnbackedTextDyldFailure,
|
||||
nativeMachoCapturedOutput,
|
||||
missingUuidLimitation,
|
||||
unusableNativeCommandOutput,
|
||||
} from "./NativeMachoCommandDiagnostics.js";
|
||||
@@ -110,7 +115,11 @@ export const inspectNativeMacho = async (
|
||||
context.target.path,
|
||||
],
|
||||
context.signal,
|
||||
tool === "dyld_info" ? isMissingUuidDyldFailure : undefined,
|
||||
tool === "dyld_info"
|
||||
? (capture) =>
|
||||
isMissingUuidDyldFailure(capture) ||
|
||||
isUnbackedTextDyldFailure(capture)
|
||||
: undefined,
|
||||
);
|
||||
// vtool knows no variant subtypes, so no spelling selects an arm64e.x1 slice.
|
||||
const vtoolSelectsSlice =
|
||||
@@ -129,7 +138,8 @@ export const inspectNativeMacho = async (
|
||||
if (!captured.ok) return err(captured.error);
|
||||
if (
|
||||
(captured.value.exitCode !== 0 || captured.value.signal !== null) &&
|
||||
!isMissingUuidDyldFailure(captured.value)
|
||||
!isMissingUuidDyldFailure(captured.value) &&
|
||||
!isUnbackedTextDyldFailure(captured.value)
|
||||
)
|
||||
return err(unusableNativeCommandOutput(captured.value));
|
||||
captures.push(captured.value);
|
||||
@@ -170,15 +180,24 @@ export const inspectNativeMacho = async (
|
||||
"Malformed otool load-command output",
|
||||
{
|
||||
cause,
|
||||
capturedOutput: {
|
||||
stdout: otool.stdout,
|
||||
stderr: otool.stderr,
|
||||
truncated: false,
|
||||
stdout_bytes: otool.stdoutBytes,
|
||||
stderr_bytes: otool.stderrBytes,
|
||||
exit_code: otool.exitCode,
|
||||
signal: otool.signal,
|
||||
},
|
||||
capturedOutput: nativeMachoCapturedOutput(otool),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
const unbackedTextFailure = captures.find(isUnbackedTextDyldFailure);
|
||||
if (unbackedTextFailure !== undefined) {
|
||||
if (!["10", "DSYM", "MH_DSYM"].includes(load.fileType ?? ""))
|
||||
return err(unusableNativeCommandOutput(unbackedTextFailure));
|
||||
return err(
|
||||
new AnalysisUnsupportedTargetError(
|
||||
"inspect_macho",
|
||||
context.target.path,
|
||||
"The selected Mach-O is a dSYM companion whose unbacked text sections are unsupported by dyld_info inspection",
|
||||
{
|
||||
capturedOutput: nativeMachoCapturedOutput(unbackedTextFailure),
|
||||
remediationAction:
|
||||
"Select the matching linked Mach-O executable for native inspection; this operation does not inspect dSYM companions with unbacked text sections.",
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user