mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
refactor(macho): share bounded header declarations
This commit is contained in:
1 parent
f8c5a6cc10
commit
0f64f1f855
6 files changed
+130
-47
No files matched your search
@@ -1866,3 +1866,21 @@ it.each([true, false])(
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("observes only the common 28-byte header of a 64-bit slice", async () => {
|
||||
const bytes = machoImage({ fileType: FILE_TYPE.dylib }).subarray(0, 28);
|
||||
const readAt = vi.fn(readerOf(bytes));
|
||||
expect(await readMachoImage(readAt, 32)).toMatchObject({
|
||||
status: "parsed",
|
||||
slices: [{ file_type: "dylib", file_type_code: FILE_TYPE.dylib }],
|
||||
});
|
||||
expect(readAt.mock.calls).toEqual([
|
||||
[0, 8],
|
||||
[0, 28],
|
||||
[32, 0],
|
||||
]);
|
||||
expect(await readMachoImage(readerOf(bytes), 28)).toEqual({
|
||||
status: "malformed",
|
||||
reason: "load commands extend beyond the Mach-O slice",
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,8 @@
|
||||
import { readMachoSegmentHeader } from "../../domain/apple/machoSegmentHeader.js";
|
||||
import { MachoLoadCommandCursor } from "../../domain/apple/machoLoadCommands.js";
|
||||
import {
|
||||
MachoLoadCommandCursor,
|
||||
readMachoHeaderDeclarations,
|
||||
} from "../../domain/apple/machoLoadCommands.js";
|
||||
import {
|
||||
MAX_JVM_CLASS_IDENTITY_BYTES,
|
||||
JVM_CLASS_IDENTITY_LIMIT_REASON,
|
||||
@@ -21,7 +24,6 @@ import {
|
||||
classifyFatContainerIdentity,
|
||||
readFatContainerHeader,
|
||||
readFatSliceDeclarations,
|
||||
readMachoHeaderPrefixFacts,
|
||||
validateFatSlice,
|
||||
validateFatSliceIdentity,
|
||||
type FatSliceDeclaration,
|
||||
@@ -312,24 +314,13 @@ const readSlice = async (
|
||||
readonly alignmentExponent: number;
|
||||
} | null = null,
|
||||
): Promise<MachoSlice> => {
|
||||
const header = viewOf(await readExact(readAt, base, 28, "Mach-O header"));
|
||||
const headerBytes = new Uint8Array(
|
||||
header.buffer,
|
||||
header.byteOffset,
|
||||
header.byteLength,
|
||||
);
|
||||
const identity = readMachoHeaderPrefixFacts(headerBytes).identity;
|
||||
const magic = header.getUint32(0, true);
|
||||
if (magic === MH_MAGIC_64 || magic === MH_MAGIC) {
|
||||
if (identity === null)
|
||||
throw new MachoFormatIssue(
|
||||
"malformed",
|
||||
`slice at offset ${base} has no Mach-O header`,
|
||||
);
|
||||
const wide = magic === MH_MAGIC_64;
|
||||
const headerSize = wide ? 32 : 28;
|
||||
const commandCount = header.getUint32(16, true);
|
||||
const commandBytes = header.getUint32(20, true);
|
||||
const header = await readExact(readAt, base, 28, "Mach-O header");
|
||||
const parsed = readMachoHeaderDeclarations(header, length);
|
||||
if (!parsed.ok)
|
||||
throw new MachoFormatIssue("malformed", "Mach-O header is truncated");
|
||||
if (parsed.value !== null && parsed.value.littleEndian) {
|
||||
const { wide, headerSize, commandCount, commandBytes, fileType, identity } =
|
||||
parsed.value;
|
||||
if (commandBytes > MAX_LOAD_COMMAND_BYTES)
|
||||
throw new MachoFormatIssue(
|
||||
"unsupported",
|
||||
@@ -350,7 +341,7 @@ const readSlice = async (
|
||||
wide,
|
||||
cpuType: identity.cpuType,
|
||||
cpuSubtype: identity.cpuSubtype,
|
||||
fileType: header.getUint32(12, true),
|
||||
fileType,
|
||||
commandCount,
|
||||
sliceOffset: base,
|
||||
sliceSize: length,
|
||||
@@ -373,10 +364,10 @@ const readSlice = async (
|
||||
return slice;
|
||||
}
|
||||
throw new MachoFormatIssue(
|
||||
magic === swap(MH_MAGIC) || magic === swap(MH_MAGIC_64)
|
||||
parsed.value !== null && !parsed.value.littleEndian
|
||||
? "unsupported"
|
||||
: "malformed",
|
||||
magic === swap(MH_MAGIC) || magic === swap(MH_MAGIC_64)
|
||||
parsed.value !== null && !parsed.value.littleEndian
|
||||
? "big-endian Mach-O slices are not supported"
|
||||
: `slice at offset ${base} has no Mach-O header`,
|
||||
);
|
||||
@@ -1021,12 +1012,5 @@ const fileTypeName = (fileType: number): MachoSlice["file_type"] =>
|
||||
const version = (value: number): string =>
|
||||
`${value >>> 16}.${(value >>> 8) & 0xff}.${value & 0xff}`;
|
||||
|
||||
const swap = (value: number): number =>
|
||||
(((value & 0xff) << 24) |
|
||||
((value & 0xff00) << 8) |
|
||||
((value >>> 8) & 0xff00) |
|
||||
(value >>> 24)) >>>
|
||||
0;
|
||||
|
||||
const viewOf = (bytes: Uint8Array): DataView =>
|
||||
new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
@@ -0,0 +1,59 @@
|
||||
import { expect, it } from "vitest";
|
||||
import {
|
||||
readMachoHeaderDeclarations,
|
||||
readMachoLoadCommandDeclarations,
|
||||
readMachoLoadCommandTableHeader,
|
||||
} from "./machoLoadCommands.js";
|
||||
|
||||
it.each([
|
||||
{ magic: 0xfeedface, wide: false, littleEndian: false },
|
||||
{ magic: 0xcefaedfe, wide: false, littleEndian: true },
|
||||
{ magic: 0xfeedfacf, wide: true, littleEndian: false },
|
||||
{ magic: 0xcffaedfe, wide: true, littleEndian: true },
|
||||
])(
|
||||
"separates common header observations from full header and table admission ($magic)",
|
||||
({ magic, wide, littleEndian }) => {
|
||||
const backing = Buffer.alloc(43, 0xa5);
|
||||
const bytes = backing.subarray(7, 35);
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
view.setUint32(0, magic, false);
|
||||
view.setUint32(4, 0xfedcba98, littleEndian);
|
||||
view.setUint32(8, 0x89abcdef, littleEndian);
|
||||
view.setUint32(12, 0xffffffff, littleEndian);
|
||||
view.setUint32(16, 0x80000001, littleEndian);
|
||||
view.setUint32(20, 0xffffffff, littleEndian);
|
||||
const expected = {
|
||||
headerSize: wide ? 32 : 28,
|
||||
wide,
|
||||
littleEndian,
|
||||
commandCount: 0x80000001,
|
||||
commandBytes: 0xffffffff,
|
||||
fileType: 0xffffffff,
|
||||
identity: { cpuType: 0xfedcba98, cpuSubtype: 0x89abcdef },
|
||||
};
|
||||
expect(readMachoHeaderDeclarations(bytes, 32)).toEqual({
|
||||
ok: true,
|
||||
value: expected,
|
||||
});
|
||||
expect(readMachoLoadCommandDeclarations(bytes, 32)).toEqual(
|
||||
wide
|
||||
? {
|
||||
ok: false,
|
||||
error:
|
||||
"truncated Mach-O header: a 64-bit header needs 32 bytes; the file has 32",
|
||||
}
|
||||
: { ok: true, value: expected },
|
||||
);
|
||||
const complete = backing.subarray(7, wide ? 39 : 35);
|
||||
expect(readMachoLoadCommandDeclarations(complete, 32)).toEqual({
|
||||
ok: true,
|
||||
value: expected,
|
||||
});
|
||||
expect(readMachoLoadCommandTableHeader(complete, 32)).toMatchObject({
|
||||
ok: false,
|
||||
});
|
||||
expect(
|
||||
readMachoHeaderDeclarations(bytes.subarray(0, 27), 32),
|
||||
).toMatchObject({ ok: false });
|
||||
},
|
||||
);
|
||||
@@ -1,5 +1,8 @@
|
||||
import { err, ok, type Result } from "../result.js";
|
||||
import { machoLoadCommandAlignment } from "./machoContainer.js";
|
||||
import {
|
||||
machoLoadCommandAlignment,
|
||||
readMachoHeaderPrefixFacts,
|
||||
} from "./machoContainer.js";
|
||||
|
||||
/** Header commitments needed to walk commands without retaining their payloads. */
|
||||
export interface MachoLoadCommandTableHeader {
|
||||
@@ -10,23 +13,31 @@ export interface MachoLoadCommandTableHeader {
|
||||
readonly commandBytes: number;
|
||||
}
|
||||
|
||||
/** Decode thin-header declarations without admitting their table extent. */
|
||||
export const readMachoLoadCommandDeclarations = (
|
||||
export interface MachoHeaderDeclarations extends MachoLoadCommandTableHeader {
|
||||
readonly fileType: number;
|
||||
readonly identity: { readonly cpuType: number; readonly cpuSubtype: number };
|
||||
}
|
||||
|
||||
/** Decode the common 28-byte header without requiring the 64-bit reserved word. */
|
||||
export const readMachoHeaderDeclarations = (
|
||||
bytes: Uint8Array,
|
||||
fileSize: number,
|
||||
): Result<MachoLoadCommandTableHeader | null, string> => {
|
||||
if (bytes.byteLength < 4) return ok(null);
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
const magic = view.getUint32(0, false);
|
||||
if (![0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe].includes(magic))
|
||||
): Result<MachoHeaderDeclarations | null, string> => {
|
||||
const prefix = readMachoHeaderPrefixFacts(bytes);
|
||||
const magic = prefix.magic;
|
||||
if (
|
||||
magic === null ||
|
||||
![0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe].includes(magic)
|
||||
)
|
||||
return ok(null);
|
||||
const wide = magic === 0xfeedfacf || magic === 0xcffaedfe;
|
||||
const headerSize = wide ? 32 : 28;
|
||||
if (bytes.byteLength < headerSize)
|
||||
if (bytes.byteLength < 28 || prefix.identity === null)
|
||||
return err(
|
||||
`truncated Mach-O header: a ${wide ? 64 : 32}-bit header needs ${headerSize} bytes; the file has ${fileSize}`,
|
||||
);
|
||||
const littleEndian = magic === 0xcefaedfe || magic === 0xcffaedfe;
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
const commandBytes = view.getUint32(20, littleEndian);
|
||||
return ok({
|
||||
headerSize,
|
||||
@@ -34,9 +45,26 @@ export const readMachoLoadCommandDeclarations = (
|
||||
littleEndian,
|
||||
commandCount: view.getUint32(16, littleEndian),
|
||||
commandBytes,
|
||||
fileType: view.getUint32(12, littleEndian),
|
||||
identity: prefix.identity,
|
||||
});
|
||||
};
|
||||
|
||||
/** Require the full declared 28/32-byte thin header, without admitting its command table. */
|
||||
export const readMachoLoadCommandDeclarations = (
|
||||
bytes: Uint8Array,
|
||||
fileSize: number,
|
||||
): Result<MachoHeaderDeclarations | null, string> => {
|
||||
const parsed = readMachoHeaderDeclarations(bytes, fileSize);
|
||||
if (!parsed.ok || parsed.value === null) return parsed;
|
||||
const { wide, headerSize } = parsed.value;
|
||||
if (bytes.byteLength < headerSize)
|
||||
return err(
|
||||
`truncated Mach-O header: a ${wide ? 64 : 32}-bit header needs ${headerSize} bytes; the file has ${fileSize}`,
|
||||
);
|
||||
return parsed;
|
||||
};
|
||||
|
||||
/** Admit the declared command table within the complete file's extent. */
|
||||
export const readMachoLoadCommandTableHeader = (
|
||||
bytes: Uint8Array,
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
import { machoCpuSubtypeBase } from "../domain/apple/machoCpuIdentity.js";
|
||||
import {
|
||||
readFatSliceDeclarations,
|
||||
readMachoHeaderPrefixFacts,
|
||||
validateFatSlice,
|
||||
validateFatSliceIdentity,
|
||||
type FatSliceDeclaration,
|
||||
@@ -180,12 +179,7 @@ export const parseMachoLayout = (
|
||||
);
|
||||
}
|
||||
const table = declarations.value;
|
||||
const identity = readMachoHeaderPrefixFacts(selectedBytes).identity;
|
||||
if (identity === null)
|
||||
throw new TypeError(
|
||||
"Only little-endian 64-bit Mach-O metadata is supported",
|
||||
);
|
||||
const { cpuType, cpuSubtype } = identity;
|
||||
const { cpuType, cpuSubtype } = table.identity;
|
||||
const fat = selection.fatDeclaration;
|
||||
if (fat !== undefined) {
|
||||
const issue = validateFatSliceIdentity(fat, {
|
||||
|
||||
@@ -150,7 +150,7 @@ export const readMachoImage = (bytes: Buffer): BinaryContainer => {
|
||||
}
|
||||
const complete = commands.finish();
|
||||
if (!complete.ok) throw new BinaryFormatFailure("malformed", complete.error);
|
||||
const cpu = reader.u32(4);
|
||||
const cpu = table.identity.cpuType;
|
||||
const names = new Map([
|
||||
[7, "386"],
|
||||
[0x01000007, "amd64"],
|
||||
|
||||
Reference in new issue
Block a user