fix(native): preserve dispatch metadata format refusals (#8013)
* fix(native): preserve dispatch metadata format refusals * test(native): normalize CLI format refusal envelopes * test(native): assert admitted metadata target paths
This commit is contained in:
1 parent
17ceb77ca5
commit
e3d8d20236
10 files changed
+247
-7
No files matched your search
@@ -757,6 +757,11 @@ complete observation.
|
||||
files and universal slices before metadata decoding. CPU subtypes and
|
||||
capability bits remain source facts; an `arm64` request may select an
|
||||
`arm64e` image under the existing slice preference policy.
|
||||
Objective-C class, protocol and category lists require eight-byte entries;
|
||||
Swift type and conformance sections require four-byte entries. Misaligned
|
||||
section lengths and unsupported non-PowerPC big-endian CPU declarations return
|
||||
`unsupported_target` with the selected path and precise reason. Recovery
|
||||
selects a well-formed or supported artifact and reopens that target.
|
||||
The byte reader holds the whole selected file in memory, so it admits files up
|
||||
to 64 MiB / 67108864 bytes by default. Select a larger budget with
|
||||
`max_input_bytes` (CLI `--max-input-bytes`), at most 1 GiB / 1073741824
|
||||
|
||||
@@ -10,10 +10,14 @@ import { ArtifactResourceScope } from "../../../dist/artifacts/ArtifactResourceS
|
||||
import { inspectAppleDispatchMetadata } from "../../../dist/native/AppleDispatchMetadata.js";
|
||||
import {
|
||||
artifactCliEvidence,
|
||||
artifactMcpCall,
|
||||
artifactMcpResult,
|
||||
withArtifactMcp,
|
||||
} from "../../lib/artifact-e2e.mjs";
|
||||
import { nativeDispatchMetadataResultSchema } from "../../../dist/domain/native/objcSwiftMetadata.js";
|
||||
import { analysisErrorProjectionSchema } from "../../../dist/contracts/errorSchemas.js";
|
||||
import { cliOperationFailurePayload } from "../../../dist/cliLogging.js";
|
||||
import { requireMcpToolError } from "../../lib/mcp-verifier-results.mjs";
|
||||
|
||||
if (process.platform !== "darwin")
|
||||
throw new Error(
|
||||
@@ -344,6 +348,74 @@ try {
|
||||
commands,
|
||||
target_sha256: cliResult.target_sha256,
|
||||
});
|
||||
for (const { section, size, reason } of [
|
||||
{
|
||||
section: "__objc_classlist",
|
||||
size: 12,
|
||||
reason: "Misaligned Objective-C class-list size",
|
||||
},
|
||||
{
|
||||
section: "__swift5_types",
|
||||
size: 6,
|
||||
reason: "Misaligned Swift type section",
|
||||
},
|
||||
]) {
|
||||
const bytes = Buffer.alloc(4096);
|
||||
bytes.writeUInt32LE(0xfeedfacf);
|
||||
bytes.writeUInt32LE(0x0100000c, 4);
|
||||
bytes.writeUInt32LE(2, 12);
|
||||
bytes.writeUInt32LE(1, 16);
|
||||
bytes.writeUInt32LE(152, 20);
|
||||
bytes.writeUInt32LE(0x19, 32); // LC_SEGMENT_64
|
||||
bytes.writeUInt32LE(152, 36);
|
||||
bytes.write("__DATA", 40);
|
||||
bytes.writeBigUInt64LE(0x100000000n, 56);
|
||||
bytes.writeBigUInt64LE(4096n, 64);
|
||||
bytes.writeBigUInt64LE(4096n, 80);
|
||||
bytes.writeUInt32LE(7, 88);
|
||||
bytes.writeUInt32LE(5, 92);
|
||||
bytes.writeUInt32LE(1, 96);
|
||||
bytes.write(section, 104);
|
||||
bytes.write("__DATA", 120);
|
||||
bytes.writeBigUInt64LE(0x100000200n, 136);
|
||||
bytes.writeBigUInt64LE(BigInt(size), 144);
|
||||
bytes.writeUInt32LE(512, 152);
|
||||
const path = join(root, `malformed-${section}.macho`);
|
||||
await writeFile(path, bytes);
|
||||
const target = await parseBinaryTarget(path);
|
||||
assert.ok(
|
||||
target.ok,
|
||||
"Malformed section fixture must pass target admission",
|
||||
);
|
||||
let cliFailure;
|
||||
try {
|
||||
await artifactCliEvidence("inspect-native-dispatch-metadata", path);
|
||||
assert.fail("Malformed metadata unexpectedly succeeded");
|
||||
} catch (cause) {
|
||||
assert.equal(cause.code, 1);
|
||||
cliFailure = analysisErrorProjectionSchema.parse(
|
||||
cliOperationFailurePayload(JSON.parse(cause.stdout)),
|
||||
);
|
||||
}
|
||||
assert.equal(cliFailure.code, "unsupported_target");
|
||||
assert.equal(cliFailure.category, "unsupported_target");
|
||||
assert.deepEqual(cliFailure.details, {
|
||||
operation: "inspect_native_dispatch_metadata",
|
||||
path: target.value.path,
|
||||
reason,
|
||||
});
|
||||
await withArtifactMcp(path, async (client) => {
|
||||
const failure = requireMcpToolError(
|
||||
await artifactMcpCall(client, {
|
||||
name: "inspect_native_dispatch_metadata",
|
||||
arguments: {},
|
||||
}),
|
||||
);
|
||||
assert.equal(failure.code, cliFailure.code);
|
||||
assert.deepEqual(failure.details, cliFailure.details);
|
||||
});
|
||||
results.push({ variant: `malformed-${section}`, reason });
|
||||
}
|
||||
for (const mode of LINK_MODES) {
|
||||
const objcPath = join(root, `fixture-${mode.mode}`);
|
||||
await exec("/usr/bin/xcrun", [
|
||||
|
||||
@@ -1,6 +1,28 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
||||
import { appleDispatchMetadataFailure } from "./AppleDispatchMetadata.js";
|
||||
import { BinaryFormatFailure } from "./metadata/BinaryContainer.js";
|
||||
|
||||
it.each(["malformed", "unsupported"] as const)(
|
||||
"keeps an explicit %s metadata refusal target-specific",
|
||||
(kind) => {
|
||||
const cause = new BinaryFormatFailure(kind, "metadata format refused");
|
||||
expect(appleDispatchMetadataFailure(cause, "/selected")).toMatchObject({
|
||||
_tag: "AnalysisUnsupportedTargetError",
|
||||
path: "/selected",
|
||||
reason: "metadata format refused",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([new RangeError("unexpected range"), new TypeError("unexpected type")])(
|
||||
"keeps an unclassified parser exception as a provider failure (%s)",
|
||||
(cause) => {
|
||||
expect(appleDispatchMetadataFailure(cause, "/selected")._tag).toBe(
|
||||
"ProviderAdapterError",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// The metadata target is already bound, so losing it is a change to that artifact rather than a new input error.
|
||||
it.each(["integrity", "path"] as const)(
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
AnalysisAccessDeniedError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisResourceConstraintError,
|
||||
AnalysisUnsupportedTargetError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
ArtifactFileCleanupFailure,
|
||||
@@ -41,6 +42,7 @@ import { readBoundedFileBytes } from "../process/BoundedFileBytes.js";
|
||||
import { EvidenceIntegrityError } from "../domain/evidenceErrors.js";
|
||||
import { pushDispatchCoverage } from "./AppleDispatchCoverage.js";
|
||||
import { parsePointerFixups } from "./AppleMachoFixups.js";
|
||||
import { BinaryFormatFailure } from "./metadata/BinaryContainer.js";
|
||||
import {
|
||||
parseMachoLayout,
|
||||
UnsupportedMachoMetadataFormatError,
|
||||
@@ -230,7 +232,10 @@ export const decodeAppleDispatchMetadata = (
|
||||
({ name }) => name === "__swift5_types",
|
||||
)) {
|
||||
if (section.size % 4 !== 0)
|
||||
throw new RangeError("Misaligned Swift type section");
|
||||
throw new BinaryFormatFailure(
|
||||
"malformed",
|
||||
"Misaligned Swift type section",
|
||||
);
|
||||
for (let index = 0; index < section.size / 4 && index < maxRecords; index++)
|
||||
typeEntries.push(section.address + BigInt(index * 4));
|
||||
if (section.size / 4 > maxRecords) budget.truncated = true;
|
||||
@@ -445,6 +450,19 @@ const appleDispatchMetadataPrimaryFailure = (
|
||||
): AnalysisError => {
|
||||
if (origin === "owned-or-classified") {
|
||||
if (cause instanceof AnalysisError) return cause;
|
||||
if (cause instanceof BinaryFormatFailure)
|
||||
return new AnalysisUnsupportedTargetError(
|
||||
OPERATION,
|
||||
path,
|
||||
cause.message,
|
||||
{
|
||||
cause,
|
||||
remediationAction:
|
||||
cause.kind === "malformed"
|
||||
? "Select a Mach-O artifact with well-formed metadata sections, reopen the target, then retry."
|
||||
: "Select a supported little-endian 64-bit Mach-O artifact, reopen the target, then retry.",
|
||||
},
|
||||
);
|
||||
if (cause instanceof UnsupportedMachoMetadataFormatError)
|
||||
return new AnalysisCapabilityUnavailableError(
|
||||
IDENTITY.id,
|
||||
|
||||
@@ -183,7 +183,10 @@ export const parseMachoLayout = (
|
||||
}
|
||||
if (table.wide && !table.littleEndian) {
|
||||
if (cpuType !== 0x01000012)
|
||||
throw new TypeError("Unsupported big-endian Mach-O CPU type");
|
||||
throw new BinaryFormatFailure(
|
||||
"unsupported",
|
||||
"Unsupported big-endian Mach-O CPU type",
|
||||
);
|
||||
throw new UnsupportedMachoMetadataFormatError(
|
||||
"Unsupported big-endian 64-bit PowerPC Mach-O metadata",
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Section } from "./AppleMachoSelection.js";
|
||||
import type { ObjcDispatchReaders } from "./AppleObjcDispatchFacets.js";
|
||||
import { readObjcPropertiesOf } from "./AppleObjcProperties.js";
|
||||
import { issue, type DecodeIssue } from "./AppleDispatchDecodeFacts.js";
|
||||
import { BinaryFormatFailure } from "./metadata/BinaryContainer.js";
|
||||
|
||||
const CLASS_SYMBOL = /^_OBJC_(?:META)?CLASS_\$_(.+)$/u;
|
||||
|
||||
@@ -59,7 +60,10 @@ export const decodeObjcCategories = (input: {
|
||||
({ name }) => name === "__objc_catlist",
|
||||
)) {
|
||||
if (section.size % 8 !== 0)
|
||||
throw new RangeError("Misaligned Objective-C category list");
|
||||
throw new BinaryFormatFailure(
|
||||
"malformed",
|
||||
"Misaligned Objective-C category list",
|
||||
);
|
||||
for (let index = 0; index < section.size / 8; index++) {
|
||||
if (!input.admit()) break;
|
||||
examined++;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import type { Segment, Section } from "./AppleMachoSelection.js";
|
||||
import { boundClassName, decodeObjcCategories } from "./AppleObjcCategories.js";
|
||||
import { readObjcPropertiesOf } from "./AppleObjcProperties.js";
|
||||
import { BinaryFormatFailure } from "./metadata/BinaryContainer.js";
|
||||
import type { DecodeIssue } from "./AppleDispatchDecodeFacts.js";
|
||||
import { createObjcProtocolReader } from "./AppleObjcProtocols.js";
|
||||
import type {
|
||||
@@ -250,7 +251,10 @@ export const decodeObjcDispatchFacets = (input: {
|
||||
({ name }) => name === "__objc_protolist",
|
||||
)) {
|
||||
if (section.size % 8 !== 0)
|
||||
throw new RangeError("Misaligned Objective-C protocol list");
|
||||
throw new BinaryFormatFailure(
|
||||
"malformed",
|
||||
"Misaligned Objective-C protocol list",
|
||||
);
|
||||
for (let index = 0; index < section.size / 8; index++) {
|
||||
if (budget.isFull()) {
|
||||
budget.markTruncated();
|
||||
@@ -406,7 +410,10 @@ export const decodeObjcDispatchFacets = (input: {
|
||||
({ name }) => name === "__objc_classlist",
|
||||
)) {
|
||||
if (section.size % 8 !== 0)
|
||||
throw new RangeError("Misaligned Objective-C class-list size");
|
||||
throw new BinaryFormatFailure(
|
||||
"malformed",
|
||||
"Misaligned Objective-C class-list size",
|
||||
);
|
||||
for (let index = 0; index < section.size / 8; index++) {
|
||||
if (budget.isFull()) {
|
||||
budget.markTruncated();
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
import type { ObjcSwiftMetadata } from "../domain/native/objcSwiftMetadata.js";
|
||||
import type { PointerResolution } from "./AppleMachoFixups.js";
|
||||
import type { Segment, Section } from "./AppleMachoSelection.js";
|
||||
import { BinaryFormatFailure } from "./metadata/BinaryContainer.js";
|
||||
import { issue, type FacetDecodeFacts } from "./AppleDispatchDecodeFacts.js";
|
||||
import {
|
||||
decodeSwiftConformanceTypeReference,
|
||||
@@ -325,7 +326,10 @@ export const decodeSwiftDispatchFacets = (input: {
|
||||
({ name }) => name === "__swift5_proto",
|
||||
)) {
|
||||
if (section.size % 4 !== 0)
|
||||
throw new RangeError("Misaligned Swift conformance section");
|
||||
throw new BinaryFormatFailure(
|
||||
"malformed",
|
||||
"Misaligned Swift conformance section",
|
||||
);
|
||||
for (let index = 0; index < section.size / 4; index++) {
|
||||
if (!budget.admit()) break;
|
||||
swiftExamined++;
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
|
||||
import { NativeMacOSProvider } from "../../../../src/native/NativeMacOSProvider.js";
|
||||
import {
|
||||
NativeFixtureRunner,
|
||||
nativeMachoTargetForFile,
|
||||
} from "../../../fixtures/nativeCommands.js";
|
||||
import { objcRecordBudgetMacho } from "../../../fixtures/objcRecordBudgetMacho.js";
|
||||
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const sectionCases = [
|
||||
{
|
||||
section: "__objc_classlist",
|
||||
size: 12,
|
||||
reason: "Misaligned Objective-C class-list size",
|
||||
},
|
||||
{
|
||||
section: "__objc_protolist",
|
||||
size: 12,
|
||||
reason: "Misaligned Objective-C protocol list",
|
||||
},
|
||||
{
|
||||
section: "__objc_catlist",
|
||||
size: 12,
|
||||
reason: "Misaligned Objective-C category list",
|
||||
},
|
||||
{
|
||||
section: "__swift5_types",
|
||||
size: 6,
|
||||
reason: "Misaligned Swift type section",
|
||||
},
|
||||
{
|
||||
section: "__swift5_proto",
|
||||
size: 6,
|
||||
reason: "Misaligned Swift conformance section",
|
||||
},
|
||||
];
|
||||
|
||||
const inspect = async (bytes: Buffer) => {
|
||||
const root = await createTestTempDirectory("rea-dispatch-format-");
|
||||
const path = join(root, "fixture.macho");
|
||||
await writeFile(path, bytes);
|
||||
const client = new NativeMacOSProvider(
|
||||
{},
|
||||
new NativeFixtureRunner(),
|
||||
"darwin",
|
||||
).createClient(await nativeMachoTargetForFile(path));
|
||||
try {
|
||||
return {
|
||||
path,
|
||||
result: await client.execute("inspect_native_dispatch_metadata", {}),
|
||||
};
|
||||
} finally {
|
||||
expect((await client.close()).ok).toBe(true);
|
||||
}
|
||||
};
|
||||
|
||||
it.each(sectionCases)(
|
||||
"reports malformed $section bytes as a target refusal with the producing reason",
|
||||
async ({ section, size, reason }) => {
|
||||
const bytes = objcRecordBudgetMacho("class");
|
||||
bytes.fill(0, 104, 120);
|
||||
bytes.write(section, 104);
|
||||
bytes.writeBigUInt64LE(BigInt(size), 144);
|
||||
const { path, result } = await inspect(bytes);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected malformed target refusal");
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "unsupported_target",
|
||||
category: "unsupported_target",
|
||||
details: { operation: "inspect_native_dispatch_metadata", path, reason },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps an unsupported big-endian CPU declaration distinct from provider failure", async () => {
|
||||
const bytes = Buffer.alloc(32);
|
||||
bytes.writeUInt32BE(0xfeedfacf);
|
||||
bytes.writeUInt32BE(0x0100000c, 4);
|
||||
bytes.writeUInt32BE(2, 12);
|
||||
const { path, result } = await inspect(bytes);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected unsupported target refusal");
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "unsupported_target",
|
||||
category: "unsupported_target",
|
||||
details: { path, reason: "Unsupported big-endian Mach-O CPU type" },
|
||||
});
|
||||
});
|
||||
|
||||
it("continues decoding the well-formed eight-byte class-list control", async () => {
|
||||
const { result } = await inspect(objcRecordBudgetMacho("class"));
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.result).toMatchObject({
|
||||
result: {
|
||||
objc_classes: [expect.objectContaining({ name: "FixtureClass" })],
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
parseMachoLayout,
|
||||
} from "../../../../src/native/AppleMachoSelection.js";
|
||||
import { NativeMacOSProvider } from "../../../../src/native/NativeMacOSProvider.js";
|
||||
import { BinaryFormatFailure } from "../../../../src/native/metadata/BinaryContainer.js";
|
||||
import { NativeFixtureRunner } from "../../../fixtures/nativeCommands.js";
|
||||
import { ok } from "../../../../src/domain/result.js";
|
||||
|
||||
@@ -171,7 +172,8 @@ describe("native Objective-C class metadata capability fallback", () => {
|
||||
} catch (cause: unknown) {
|
||||
mismatchedCpuFailure = cause;
|
||||
}
|
||||
expect(mismatchedCpuFailure).toBeInstanceOf(TypeError);
|
||||
expect(mismatchedCpuFailure).toBeInstanceOf(BinaryFormatFailure);
|
||||
expect(mismatchedCpuFailure).toMatchObject({ kind: "unsupported" });
|
||||
expect(mismatchedCpuFailure).not.toBeInstanceOf(
|
||||
UnsupportedMachoMetadataFormatError,
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user