mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 03:02:56 +00:00
fix(native): classify malformed dispatch metadata
This commit is contained in:
1 parent
823f50ff03
commit
d97aa0849d
7 files changed
+248
-25
No files matched your search
@@ -0,0 +1,104 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import {
|
||||
appleDispatchMetadataFailure,
|
||||
decodeAppleDispatchMetadata,
|
||||
} from "./AppleDispatchMetadata.js";
|
||||
import { parseMachoLayout } from "./AppleMachoSelection.js";
|
||||
|
||||
const provenance = { path: "/selected", sha256: "fixture" };
|
||||
|
||||
const metadataSection = (name: string, size: number): Buffer => {
|
||||
const bytes = Buffer.alloc(4096);
|
||||
bytes.writeUInt32LE(0xfeedfacf, 0);
|
||||
bytes.writeUInt32LE(0x0100000c, 4);
|
||||
bytes.writeUInt32LE(2, 12);
|
||||
bytes.writeUInt32LE(1, 16);
|
||||
bytes.writeUInt32LE(152, 20);
|
||||
bytes.writeUInt32LE(0x19, 32);
|
||||
bytes.writeUInt32LE(152, 36);
|
||||
bytes.write("__DATA", 40);
|
||||
bytes.writeBigUInt64LE(0x100000000n, 56);
|
||||
bytes.writeBigUInt64LE(4096n, 64);
|
||||
bytes.writeBigUInt64LE(4096n, 80);
|
||||
bytes.writeUInt32LE(1, 96);
|
||||
bytes.write(name, 104);
|
||||
bytes.write("__DATA", 120);
|
||||
bytes.writeBigUInt64LE(0x100000200n, 136);
|
||||
bytes.writeBigUInt64LE(BigInt(size), 144);
|
||||
bytes.writeUInt32LE(512, 152);
|
||||
return bytes;
|
||||
};
|
||||
|
||||
it.each([
|
||||
["__objc_classlist", 12, "Misaligned Objective-C class-list size"],
|
||||
["__objc_protolist", 12, "Misaligned Objective-C protocol list"],
|
||||
["__objc_catlist", 12, "Misaligned Objective-C category list"],
|
||||
["__swift5_types", 6, "Misaligned Swift type section"],
|
||||
["__swift5_proto", 6, "Misaligned Swift conformance section"],
|
||||
] as const)(
|
||||
"classifies malformed %s as a target refusal",
|
||||
(name, size, reason) => {
|
||||
let thrown: unknown;
|
||||
try {
|
||||
decodeAppleDispatchMetadata(metadataSection(name, size), 10, provenance);
|
||||
} catch (cause) {
|
||||
thrown = cause;
|
||||
}
|
||||
expect(thrown).toBeInstanceOf(Error);
|
||||
expect(
|
||||
projectAnalysisError(
|
||||
appleDispatchMetadataFailure(thrown, provenance.path),
|
||||
),
|
||||
).toMatchObject({
|
||||
code: "unsupported_target",
|
||||
message: expect.stringContaining(reason),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("admits a complete 4097-command table and rejects truncated framing", () => {
|
||||
const count = 4097;
|
||||
const bytes = Buffer.alloc(32 + count * 16);
|
||||
bytes.writeUInt32LE(0xfeedfacf, 0);
|
||||
bytes.writeUInt32LE(0x0100000c, 4);
|
||||
bytes.writeUInt32LE(2, 12);
|
||||
bytes.writeUInt32LE(count, 16);
|
||||
bytes.writeUInt32LE(count * 16, 20);
|
||||
for (let index = 0; index < count; index++) {
|
||||
bytes.writeUInt32LE(0x2a, 32 + index * 16);
|
||||
bytes.writeUInt32LE(16, 36 + index * 16);
|
||||
}
|
||||
expect(parseMachoLayout(bytes, "arm64").sections).toEqual([]);
|
||||
let thrown: unknown;
|
||||
try {
|
||||
parseMachoLayout(bytes.subarray(0, bytes.length - 1), "arm64");
|
||||
} catch (cause) {
|
||||
thrown = cause;
|
||||
}
|
||||
expect(
|
||||
projectAnalysisError(appleDispatchMetadataFailure(thrown, provenance.path)),
|
||||
).toMatchObject({
|
||||
code: "unsupported_target",
|
||||
details: { reason: "Malformed Mach-O command bounds" },
|
||||
});
|
||||
});
|
||||
|
||||
it("classifies an unsupported big-endian CPU as a target refusal", () => {
|
||||
const bytes = Buffer.alloc(32);
|
||||
bytes.writeUInt32BE(0xfeedfacf, 0);
|
||||
bytes.writeUInt32BE(0x0100000c, 4);
|
||||
let thrown: unknown;
|
||||
try {
|
||||
decodeAppleDispatchMetadata(bytes, 10, provenance);
|
||||
} catch (cause) {
|
||||
thrown = cause;
|
||||
}
|
||||
expect(
|
||||
projectAnalysisError(appleDispatchMetadataFailure(thrown, provenance.path)),
|
||||
).toMatchObject({
|
||||
code: "unsupported_target",
|
||||
message: expect.stringContaining("Unsupported big-endian Mach-O CPU type"),
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
AnalysisAccessDeniedError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisResourceConstraintError,
|
||||
AnalysisUnsupportedTargetError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
ArtifactFileCleanupFailure,
|
||||
@@ -42,6 +43,7 @@ import { EvidenceIntegrityError } from "../domain/evidenceErrors.js";
|
||||
import { pushDispatchCoverage } from "./AppleDispatchCoverage.js";
|
||||
import { parsePointerFixups } from "./AppleMachoFixups.js";
|
||||
import {
|
||||
MalformedMachoLayoutError,
|
||||
parseMachoLayout,
|
||||
UnsupportedMachoMetadataFormatError,
|
||||
} from "./AppleMachoSelection.js";
|
||||
@@ -58,6 +60,7 @@ import {
|
||||
createSwiftContextReader,
|
||||
} from "./AppleSwiftNominalTypes.js";
|
||||
import { inspectObjcClassImportBindings } from "./AppleObjcClassImportBindings.js";
|
||||
import { BinaryFormatFailure } from "./metadata/BinaryContainer.js";
|
||||
|
||||
/** Decode validated little-endian 64-bit Apple Objective-C metadata without loading target code. */
|
||||
export const decodeAppleDispatchMetadata = (
|
||||
@@ -230,7 +233,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 +451,18 @@ const appleDispatchMetadataPrimaryFailure = (
|
||||
): AnalysisError => {
|
||||
if (origin === "owned-or-classified") {
|
||||
if (cause instanceof AnalysisError) return cause;
|
||||
if (
|
||||
cause instanceof BinaryFormatFailure ||
|
||||
cause instanceof MalformedMachoLayoutError
|
||||
)
|
||||
return new AnalysisUnsupportedTargetError(
|
||||
OPERATION,
|
||||
path,
|
||||
cause.message,
|
||||
{
|
||||
cause,
|
||||
},
|
||||
);
|
||||
if (cause instanceof UnsupportedMachoMetadataFormatError)
|
||||
return new AnalysisCapabilityUnavailableError(
|
||||
IDENTITY.id,
|
||||
|
||||
@@ -141,6 +141,9 @@ export const selectMachoSlice = (
|
||||
return selected;
|
||||
};
|
||||
|
||||
/** A rejected layout declaration in the selected Mach-O image. */
|
||||
export class MalformedMachoLayoutError extends RangeError {}
|
||||
|
||||
/** A recognized Mach-O class outside this metadata reader's supported formats. */
|
||||
export class UnsupportedMachoMetadataFormatError extends Error {
|
||||
constructor(message: string) {
|
||||
@@ -154,19 +157,22 @@ export const parseMachoLayout = (
|
||||
bytes: Buffer,
|
||||
architecture: string,
|
||||
): MachoLayout => {
|
||||
if (bytes.length < 4) throw new RangeError("Truncated Mach-O header");
|
||||
if (bytes.length < 4)
|
||||
throw new MalformedMachoLayoutError("Truncated Mach-O header");
|
||||
const selection = selectMachoSlice(bytes, architecture);
|
||||
const { slice, sliceEnd } = selection;
|
||||
if (slice + 4 > sliceEnd) throw new RangeError("Truncated Mach-O header");
|
||||
if (slice + 4 > sliceEnd)
|
||||
throw new MalformedMachoLayoutError("Truncated Mach-O header");
|
||||
const selectedBytes = bytes.subarray(slice, sliceEnd);
|
||||
const declarations = readMachoLoadCommandDeclarations(
|
||||
selectedBytes,
|
||||
selectedBytes.length,
|
||||
);
|
||||
if (!declarations.ok) throw new RangeError("Truncated Mach-O header");
|
||||
if (!declarations.ok)
|
||||
throw new MalformedMachoLayoutError("Truncated Mach-O header");
|
||||
if (declarations.value === null) {
|
||||
if (selectedBytes.length < 32)
|
||||
throw new RangeError("Truncated Mach-O header");
|
||||
throw new MalformedMachoLayoutError("Truncated Mach-O header");
|
||||
throw new TypeError(
|
||||
"Only little-endian 64-bit Mach-O metadata is supported",
|
||||
);
|
||||
@@ -183,7 +189,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",
|
||||
);
|
||||
@@ -200,7 +209,7 @@ export const parseMachoLayout = (
|
||||
);
|
||||
const commandEnd = slice + table.headerSize + table.commandBytes;
|
||||
if (commandEnd > sliceEnd)
|
||||
throw new RangeError("Malformed Mach-O command bounds");
|
||||
throw new MalformedMachoLayoutError("Malformed Mach-O command bounds");
|
||||
const state: LayoutState = {
|
||||
slice,
|
||||
sliceEnd,
|
||||
@@ -218,7 +227,7 @@ export const parseMachoLayout = (
|
||||
while (framing.nextOffset !== null) {
|
||||
const cursor = slice + table.headerSize + framing.nextOffset;
|
||||
const frame = framing.read(bytes.subarray(cursor, cursor + 8));
|
||||
if (!frame.ok) throw new RangeError(frame.error);
|
||||
if (!frame.ok) throw new MalformedMachoLayoutError(frame.error);
|
||||
const command = {
|
||||
cursor,
|
||||
kind: frame.value.command,
|
||||
@@ -228,7 +237,7 @@ export const parseMachoLayout = (
|
||||
else collectFixupCommand(bytes, command, state);
|
||||
}
|
||||
const complete = framing.finish();
|
||||
if (!complete.ok) throw new RangeError(complete.error);
|
||||
if (!complete.ok) throw new MalformedMachoLayoutError(complete.error);
|
||||
const { segments, sections, chained, binds, rebases, dylibs } = state;
|
||||
const mappings = createFileMappingIndex(state.mappings, state.zeroFills);
|
||||
const offset = (address: bigint, size = 1): number => {
|
||||
@@ -300,7 +309,8 @@ const collectSegment = (
|
||||
true,
|
||||
true,
|
||||
);
|
||||
if (!parsed.ok) throw new RangeError("Truncated segment command");
|
||||
if (!parsed.ok)
|
||||
throw new MalformedMachoLayoutError("Truncated segment command");
|
||||
const {
|
||||
address,
|
||||
memorySize,
|
||||
@@ -312,9 +322,11 @@ const collectSegment = (
|
||||
headerSize,
|
||||
} = parsed.value;
|
||||
const admitted = admitMachoSegmentMapping(parsed.value, 64);
|
||||
if (!admitted.ok) throw new RangeError(admitted.error.message);
|
||||
if (!admitted.ok) throw new MalformedMachoLayoutError(admitted.error.message);
|
||||
if (fileOffset + fileSize > BigInt(state.sliceEnd - state.slice))
|
||||
throw new RangeError("Segment file range exceeds target bytes");
|
||||
throw new MalformedMachoLayoutError(
|
||||
"Segment file range exceeds target bytes",
|
||||
);
|
||||
const name = decodeMachoFixedName(nameBytes);
|
||||
state.segments.push({
|
||||
address,
|
||||
@@ -341,7 +353,7 @@ const collectSegment = (
|
||||
)
|
||||
state.baseAddress ??= address;
|
||||
if (headerSize + count * 80 > size)
|
||||
throw new RangeError("Truncated section table");
|
||||
throw new MalformedMachoLayoutError("Truncated section table");
|
||||
for (let section = 0; section < count; section++) {
|
||||
const position = cursor + headerSize + section * 80;
|
||||
const parsedSection = readMachoSectionHeader(
|
||||
@@ -349,10 +361,13 @@ const collectSegment = (
|
||||
true,
|
||||
true,
|
||||
);
|
||||
if (!parsedSection.ok) throw new RangeError("Truncated section table");
|
||||
if (!parsedSection.ok)
|
||||
throw new MalformedMachoLayoutError("Truncated section table");
|
||||
const { nameBytes, address, size: sectionSize } = parsedSection.value;
|
||||
if (sectionSize > BigInt(Number.MAX_SAFE_INTEGER))
|
||||
throw new RangeError("Section size exceeds exact numeric range");
|
||||
throw new MalformedMachoLayoutError(
|
||||
"Section size exceeds exact numeric range",
|
||||
);
|
||||
state.sections.push({
|
||||
name: decodeMachoFixedName(nameBytes),
|
||||
address,
|
||||
@@ -375,14 +390,18 @@ const collectFixupCommand = (
|
||||
const offset = state.slice + bytes.readUInt32LE(at);
|
||||
const length = bytes.readUInt32LE(at + 4);
|
||||
if (offset + length > state.sliceEnd)
|
||||
throw new RangeError("Fixup data exceeds the Mach-O slice");
|
||||
throw new MalformedMachoLayoutError(
|
||||
"Fixup data exceeds the Mach-O slice",
|
||||
);
|
||||
return { offset, size: length };
|
||||
};
|
||||
if (kind === LC_DYLD_CHAINED_FIXUPS) {
|
||||
if (size < 16) throw new RangeError("Truncated chained-fixups command");
|
||||
if (size < 16)
|
||||
throw new MalformedMachoLayoutError("Truncated chained-fixups command");
|
||||
state.chained = range(cursor + 8);
|
||||
} else if (kind === LC_DYLD_INFO || kind === LC_DYLD_INFO_ONLY) {
|
||||
if (size < 48) throw new RangeError("Truncated dyld-info command");
|
||||
if (size < 48)
|
||||
throw new MalformedMachoLayoutError("Truncated dyld-info command");
|
||||
const rebase = range(cursor + 8);
|
||||
if (rebase.size > 0) state.rebases.push(rebase);
|
||||
for (const [at, stream] of [
|
||||
@@ -400,10 +419,12 @@ const collectFixupCommand = (
|
||||
);
|
||||
if (!parsed.ok) {
|
||||
if (parsed.error._tag === "InvalidMachoDylibCommand")
|
||||
throw new RangeError("Truncated dylib command");
|
||||
throw new MalformedMachoLayoutError("Truncated dylib command");
|
||||
if (parsed.error.reason === "unterminated")
|
||||
throw new RangeError("Unterminated dylib install name");
|
||||
throw new RangeError("Dylib install name is outside its load command");
|
||||
throw new MalformedMachoLayoutError("Unterminated dylib install name");
|
||||
throw new MalformedMachoLayoutError(
|
||||
"Dylib install name is outside its load command",
|
||||
);
|
||||
}
|
||||
const { nameBytes } = parsed.value;
|
||||
state.dylibs.push(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ObjcSwiftMetadata } from "../domain/native/objcSwiftMetadata.js";
|
||||
import type { Section } from "./AppleMachoSelection.js";
|
||||
import { BinaryFormatFailure } from "./metadata/BinaryContainer.js";
|
||||
import type { ObjcDispatchReaders } from "./AppleObjcDispatchFacets.js";
|
||||
import { readObjcPropertiesOf } from "./AppleObjcProperties.js";
|
||||
import { issue, type DecodeIssue } from "./AppleDispatchDecodeFacts.js";
|
||||
@@ -43,7 +44,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++;
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type ObjcSwiftMetadata,
|
||||
} from "../domain/native/objcSwiftMetadata.js";
|
||||
import type { Segment, Section } from "./AppleMachoSelection.js";
|
||||
import { BinaryFormatFailure } from "./metadata/BinaryContainer.js";
|
||||
import { boundClassName, decodeObjcCategories } from "./AppleObjcCategories.js";
|
||||
import { readObjcPropertiesOf } from "./AppleObjcProperties.js";
|
||||
import type { DecodeIssue } from "./AppleDispatchDecodeFacts.js";
|
||||
@@ -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,65 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
describe.skipIf(process.platform !== "darwin")(
|
||||
"native dispatch format admission",
|
||||
() => {
|
||||
cliTest(
|
||||
"accepts a walkable 4097-command table and classifies malformed section sizes",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-dispatch-format-");
|
||||
const commands = Buffer.alloc(32 + 4097 * 16);
|
||||
commands.writeUInt32LE(0xfeedfacf, 0);
|
||||
commands.writeUInt32LE(0x0100000c, 4);
|
||||
commands.writeUInt32LE(2, 12);
|
||||
commands.writeUInt32LE(4097, 16);
|
||||
commands.writeUInt32LE(4097 * 16, 20);
|
||||
for (let index = 0; index < 4097; index++) {
|
||||
commands.writeUInt32LE(0x2a, 32 + index * 16);
|
||||
commands.writeUInt32LE(16, 36 + index * 16);
|
||||
}
|
||||
const path = join(root, "commands.macho");
|
||||
await writeFile(path, commands);
|
||||
const valid = await cli.run({
|
||||
arguments: ["inspect-native-dispatch-metadata", path, "--json"],
|
||||
});
|
||||
expect(valid.exitCode, valid.stdout).toBe(0);
|
||||
expect(valid.json).toMatchObject({
|
||||
normalized_result: { result: { objc_classes: [] } },
|
||||
});
|
||||
|
||||
const malformed = Buffer.alloc(4096);
|
||||
malformed.writeUInt32LE(0xfeedfacf, 0);
|
||||
malformed.writeUInt32LE(0x0100000c, 4);
|
||||
malformed.writeUInt32LE(2, 12);
|
||||
malformed.writeUInt32LE(1, 16);
|
||||
malformed.writeUInt32LE(152, 20);
|
||||
malformed.writeUInt32LE(0x19, 32);
|
||||
malformed.writeUInt32LE(152, 36);
|
||||
malformed.write("__DATA", 40);
|
||||
malformed.writeBigUInt64LE(0x100000000n, 56);
|
||||
malformed.writeBigUInt64LE(4096n, 64);
|
||||
malformed.writeBigUInt64LE(4096n, 80);
|
||||
malformed.writeUInt32LE(1, 96);
|
||||
malformed.write("__objc_classlist", 104);
|
||||
malformed.write("__DATA", 120);
|
||||
malformed.writeBigUInt64LE(0x100000200n, 136);
|
||||
malformed.writeBigUInt64LE(12n, 144);
|
||||
malformed.writeUInt32LE(512, 152);
|
||||
await writeFile(path, malformed);
|
||||
const refused = await cli.run({
|
||||
arguments: ["inspect-native-dispatch-metadata", path, "--json"],
|
||||
});
|
||||
expect(refused.exitCode, refused.stdout).toBe(1);
|
||||
expect(refused.json).toMatchObject({
|
||||
code: "unsupported_target",
|
||||
details: { reason: "Misaligned Objective-C class-list size" },
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user