mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
refactor(macho): centralize dylib command and string facts
This commit is contained in:
1 parent
6d5532a204
commit
e405d685bc
5 files changed
+207
-63
No files matched your search
@@ -1,7 +1,13 @@
|
||||
import {
|
||||
machoDependencyCommand,
|
||||
readMachoDylibCommandFacts,
|
||||
} from "../../domain/apple/machoDylibCommand.js";
|
||||
import { readMachoSegmentHeader } from "../../domain/apple/machoSegmentHeader.js";
|
||||
import {
|
||||
MachoLoadCommandCursor,
|
||||
readMachoHeaderDeclarations,
|
||||
readMachoCommandStringBytes,
|
||||
type MachoCommandStringFailure,
|
||||
} from "../../domain/apple/machoLoadCommands.js";
|
||||
import {
|
||||
MAX_JVM_CLASS_IDENTITY_BYTES,
|
||||
@@ -53,12 +59,7 @@ export const MAX_LAZY_INFO_SYMBOL_REFERENCES = 64 * 1024;
|
||||
const LC_SEGMENT = 0x1;
|
||||
const LC_SEGMENT_64 = 0x19;
|
||||
const LC_ID_DYLIB = 0xd;
|
||||
const LC_LOAD_DYLIB = 0xc;
|
||||
const LC_LOAD_WEAK_DYLIB = 0x80000018;
|
||||
const LC_REEXPORT_DYLIB = 0x8000001f;
|
||||
const LC_LAZY_LOAD_DYLIB = 0x20;
|
||||
const LC_LAZY_LOAD_DYLIB_INFO = 0x3a;
|
||||
const LC_LOAD_UPWARD_DYLIB = 0x80000023;
|
||||
const LC_RPATH = 0x8000001c;
|
||||
const LC_DYLD_ENVIRONMENT = 0x27;
|
||||
const LC_CODE_SIGNATURE = 0x1d;
|
||||
@@ -75,8 +76,6 @@ const VERSION_MIN_PLATFORMS: ReadonlyMap<number, MachoVersionMinPlatform> =
|
||||
[0x30, 4], // LC_VERSION_MIN_WATCHOS
|
||||
]);
|
||||
|
||||
/** `mach-o/loader.h` `dylib_use_command`: `nameoff == 28` and this marker. */
|
||||
const DYLIB_USE_MARKER = 0x1a741800;
|
||||
const LAZY_INFO_HEADER_BYTES = 24;
|
||||
/** ld64 may-be-missing bit: the lazy image is allowed to be absent. */
|
||||
const LAZY_INFO_MAY_BE_MISSING = 0x1;
|
||||
@@ -90,15 +89,6 @@ const DYLIB_USE_REEXPORT = 0x2;
|
||||
const DYLIB_USE_UPWARD = 0x4;
|
||||
const DYLIB_USE_DELAYED_INIT = 0x8;
|
||||
|
||||
const DEPENDENCY_COMMANDS: ReadonlyMap<number, MachoDependency["command"]> =
|
||||
new Map([
|
||||
[LC_LOAD_DYLIB, "LC_LOAD_DYLIB"],
|
||||
[LC_LOAD_WEAK_DYLIB, "LC_LOAD_WEAK_DYLIB"],
|
||||
[LC_REEXPORT_DYLIB, "LC_REEXPORT_DYLIB"],
|
||||
[LC_LAZY_LOAD_DYLIB, "LC_LAZY_LOAD_DYLIB"],
|
||||
[LC_LOAD_UPWARD_DYLIB, "LC_LOAD_UPWARD_DYLIB"],
|
||||
]);
|
||||
|
||||
/** Malformed or unsupported structure found while decoding one image. */
|
||||
class MachoFormatIssue extends Error {
|
||||
constructor(
|
||||
@@ -634,8 +624,8 @@ const decodeCommand = (
|
||||
}
|
||||
if (command === LC_LAZY_LOAD_DYLIB_INFO)
|
||||
return decodeLazyLoadDylibInfoCommand(body, index);
|
||||
const name = DEPENDENCY_COMMANDS.get(command);
|
||||
if (name !== undefined)
|
||||
const name = machoDependencyCommand(command);
|
||||
if (name !== null)
|
||||
return { kind: "parsed", value: decodeDependency(name, body, index) };
|
||||
return undefined;
|
||||
};
|
||||
@@ -903,22 +893,21 @@ const decodeDependency = (
|
||||
body: Uint8Array,
|
||||
index: number,
|
||||
): MachoDependency => {
|
||||
if (body.byteLength < 24)
|
||||
throw new MachoFormatIssue(
|
||||
"malformed",
|
||||
`load command ${index} is too short for a dylib command`,
|
||||
);
|
||||
const view = viewOf(body);
|
||||
const nameOffset = view.getUint32(8, true);
|
||||
const usesFlags =
|
||||
nameOffset === 28 &&
|
||||
body.byteLength >= 28 &&
|
||||
view.getUint32(12, true) === DYLIB_USE_MARKER;
|
||||
const flags = usesFlags ? view.getUint32(24, true) : 0;
|
||||
const parsed = readMachoDylibCommandFacts(body, true);
|
||||
if (!parsed.ok) {
|
||||
if (parsed.error._tag === "InvalidMachoDylibCommand")
|
||||
throw new MachoFormatIssue(
|
||||
"malformed",
|
||||
`load command ${index} is too short for a dylib command`,
|
||||
);
|
||||
return throwCommandStringIssue(parsed.error, index);
|
||||
}
|
||||
const facts = parsed.value;
|
||||
const flags = facts.flags ?? 0;
|
||||
return {
|
||||
command,
|
||||
encoding: usesFlags ? "dylib_use_command" : "dylib_command",
|
||||
install_name: commandString(body, usesFlags ? 28 : 24, index),
|
||||
encoding: facts.encoding,
|
||||
install_name: decodeCommandString(facts.nameBytes, index),
|
||||
weak:
|
||||
command === "LC_LOAD_WEAK_DYLIB" || (flags & DYLIB_USE_WEAK_LINK) !== 0,
|
||||
upward:
|
||||
@@ -926,8 +915,8 @@ const decodeDependency = (
|
||||
reexport:
|
||||
command === "LC_REEXPORT_DYLIB" || (flags & DYLIB_USE_REEXPORT) !== 0,
|
||||
delayed_init: (flags & DYLIB_USE_DELAYED_INIT) !== 0,
|
||||
current_version: version(view.getUint32(16, true)),
|
||||
compatibility_version: version(view.getUint32(20, true)),
|
||||
current_version: version(facts.currentVersion),
|
||||
compatibility_version: version(facts.compatibilityVersion),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -937,27 +926,29 @@ const commandString = (
|
||||
minimumOffset: number,
|
||||
index: number,
|
||||
): string => {
|
||||
if (body.byteLength < 12)
|
||||
throw new MachoFormatIssue(
|
||||
"malformed",
|
||||
`load command ${index} is too short to hold a string offset`,
|
||||
);
|
||||
const offset = viewOf(body).getUint32(8, true);
|
||||
if (offset < minimumOffset || offset >= body.byteLength)
|
||||
throw new MachoFormatIssue(
|
||||
"malformed",
|
||||
`load command ${index} has string offset ${offset} outside the command`,
|
||||
);
|
||||
const end = body.indexOf(0, offset);
|
||||
if (end < 0)
|
||||
throw new MachoFormatIssue(
|
||||
"malformed",
|
||||
`load command ${index} has an unterminated string`,
|
||||
);
|
||||
const parsed = readMachoCommandStringBytes(body, minimumOffset, true);
|
||||
if (!parsed.ok) return throwCommandStringIssue(parsed.error, index);
|
||||
return decodeCommandString(parsed.value.bytes, index);
|
||||
};
|
||||
|
||||
const throwCommandStringIssue = (
|
||||
failure: MachoCommandStringFailure,
|
||||
index: number,
|
||||
): never => {
|
||||
const reason =
|
||||
failure.reason === "truncated-offset"
|
||||
? `load command ${index} is too short to hold a string offset`
|
||||
: failure.reason === "invalid-offset"
|
||||
? `load command ${index} has string offset ${failure.offset} outside the command`
|
||||
: `load command ${index} has an unterminated string`;
|
||||
throw new MachoFormatIssue("malformed", reason);
|
||||
};
|
||||
|
||||
const decodeCommandString = (bytes: Uint8Array, index: number): string => {
|
||||
try {
|
||||
// lc_str contains name/path data, so a leading U+FEFF is part of its identity.
|
||||
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
|
||||
body.subarray(offset, end),
|
||||
bytes,
|
||||
);
|
||||
} catch {
|
||||
throw new MachoFormatIssue(
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { MachoDependency } from "./dylibResolution.js";
|
||||
import { err, ok, type Result } from "../result.js";
|
||||
import {
|
||||
readMachoCommandStringBytes,
|
||||
type MachoCommandStringFailure,
|
||||
} from "./machoLoadCommands.js";
|
||||
|
||||
const DEPENDENCY_COMMANDS: ReadonlyMap<number, MachoDependency["command"]> =
|
||||
new Map([
|
||||
[0xc, "LC_LOAD_DYLIB"],
|
||||
[0x80000018, "LC_LOAD_WEAK_DYLIB"],
|
||||
[0x8000001f, "LC_REEXPORT_DYLIB"],
|
||||
[0x20, "LC_LAZY_LOAD_DYLIB"],
|
||||
[0x80000023, "LC_LOAD_UPWARD_DYLIB"],
|
||||
]);
|
||||
|
||||
/** Dependency order follows command order; LC_ID_DYLIB does not consume an ordinal. */
|
||||
export const machoDependencyCommand = (code: number) =>
|
||||
DEPENDENCY_COMMANDS.get(code) ?? null;
|
||||
|
||||
interface MachoDylibCommandFacts {
|
||||
readonly encoding: "dylib_command" | "dylib_use_command";
|
||||
/** Classic commands have no flags field. Unknown modern bits remain source facts. */
|
||||
readonly flags: number | null;
|
||||
readonly currentVersion: number;
|
||||
readonly compatibilityVersion: number;
|
||||
readonly timestampOrMarker: number;
|
||||
readonly nameOffset: number;
|
||||
readonly nameBytes: Uint8Array;
|
||||
}
|
||||
|
||||
/** Decode classic or marker-bearing dylib fields without imposing text or flag policy. */
|
||||
export const readMachoDylibCommandFacts = (
|
||||
body: Uint8Array,
|
||||
littleEndian: boolean,
|
||||
): Result<
|
||||
MachoDylibCommandFacts,
|
||||
| {
|
||||
readonly _tag: "InvalidMachoDylibCommand";
|
||||
readonly reason: "truncated-header";
|
||||
}
|
||||
| MachoCommandStringFailure
|
||||
> => {
|
||||
if (body.byteLength < 24)
|
||||
return err({
|
||||
_tag: "InvalidMachoDylibCommand",
|
||||
reason: "truncated-header",
|
||||
});
|
||||
const name = readMachoCommandStringBytes(body, 24, littleEndian);
|
||||
if (!name.ok) return name;
|
||||
const view = new DataView(body.buffer, body.byteOffset, body.byteLength);
|
||||
const timestampOrMarker = view.getUint32(12, littleEndian);
|
||||
// Recognition already establishes the modern minimum name offset of 28.
|
||||
const usesFlags =
|
||||
name.value.offset === 28 &&
|
||||
body.byteLength >= 28 &&
|
||||
timestampOrMarker === 0x1a741800;
|
||||
return ok({
|
||||
encoding: usesFlags ? "dylib_use_command" : "dylib_command",
|
||||
flags: usesFlags ? view.getUint32(24, littleEndian) : null,
|
||||
currentVersion: view.getUint32(16, littleEndian),
|
||||
compatibilityVersion: view.getUint32(20, littleEndian),
|
||||
timestampOrMarker,
|
||||
nameOffset: name.value.offset,
|
||||
nameBytes: name.value.bytes,
|
||||
});
|
||||
};
|
||||
@@ -134,3 +134,42 @@ export class MachoLoadCommandCursor {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export type MachoCommandStringFailure =
|
||||
| {
|
||||
readonly _tag: "InvalidMachoCommandString";
|
||||
readonly reason: "truncated-offset" | "unterminated";
|
||||
}
|
||||
| {
|
||||
readonly _tag: "InvalidMachoCommandString";
|
||||
readonly reason: "invalid-offset";
|
||||
readonly offset: number;
|
||||
};
|
||||
|
||||
/** Locate an lc_str payload within one admitted command; callers own text interpretation. */
|
||||
export const readMachoCommandStringBytes = (
|
||||
body: Uint8Array,
|
||||
minimumOffset: number,
|
||||
littleEndian: boolean,
|
||||
): Result<
|
||||
{ readonly offset: number; readonly bytes: Uint8Array },
|
||||
MachoCommandStringFailure
|
||||
> => {
|
||||
if (body.byteLength < 12)
|
||||
return err({
|
||||
_tag: "InvalidMachoCommandString",
|
||||
reason: "truncated-offset",
|
||||
});
|
||||
const view = new DataView(body.buffer, body.byteOffset, body.byteLength);
|
||||
const offset = view.getUint32(8, littleEndian);
|
||||
if (offset < minimumOffset || offset >= body.byteLength)
|
||||
return err({
|
||||
_tag: "InvalidMachoCommandString",
|
||||
reason: "invalid-offset",
|
||||
offset,
|
||||
});
|
||||
const end = body.indexOf(0, offset);
|
||||
if (end < 0)
|
||||
return err({ _tag: "InvalidMachoCommandString", reason: "unterminated" });
|
||||
return ok({ offset, bytes: body.subarray(offset, end) });
|
||||
};
|
||||
@@ -5,10 +5,12 @@ import {
|
||||
CPU,
|
||||
LC,
|
||||
dylibCommand,
|
||||
dylibUseCommand,
|
||||
fatImage,
|
||||
machoImage,
|
||||
machoImageWithUnknownCommand,
|
||||
} from "../artifacts/apple/MachoImage.fixture.js";
|
||||
import { readMachoImage } from "../artifacts/apple/MachoLoadCommandReader.js";
|
||||
import {
|
||||
parseMachoLayout,
|
||||
selectMachoSlice,
|
||||
@@ -340,6 +342,39 @@ describe("native Mach-O fixup load command boundaries", () => {
|
||||
"\ufeff/libé",
|
||||
]);
|
||||
});
|
||||
|
||||
it.each<[string, Uint8Array]>([
|
||||
["load", dylibCommand(LC.LOAD_DYLIB, "/lib?")],
|
||||
["weak", dylibCommand(LC.LOAD_WEAK_DYLIB, "/lib?")],
|
||||
["reexport", dylibCommand(LC.REEXPORT_DYLIB, "/lib?")],
|
||||
["lazy", dylibCommand(LC.LAZY_LOAD_DYLIB, "/lib?")],
|
||||
["upward", dylibCommand(LC.LOAD_UPWARD_DYLIB, "/lib?")],
|
||||
["modern use", dylibUseCommand(LC.LOAD_DYLIB, "/lib?", 0x8)],
|
||||
])(
|
||||
"retains opaque %s install-name identity while refusing text resolution",
|
||||
async (_kind, original) => {
|
||||
const command = Buffer.from(original);
|
||||
const nameOffset = command.readUInt32LE(8);
|
||||
command[nameOffset + 4] = 0x80;
|
||||
const bytes = Buffer.from(machoImage({ commands: [command] }));
|
||||
expect(parseMachoLayout(bytes, "arm64").fixups.dylibs).toEqual([
|
||||
{
|
||||
bytes_base64: Buffer.from([0x2f, 0x6c, 0x69, 0x62, 0x80]).toString(
|
||||
"base64",
|
||||
),
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
await readMachoImage(
|
||||
async (offset, length) => bytes.subarray(offset, offset + length),
|
||||
bytes.length,
|
||||
),
|
||||
).toMatchObject({
|
||||
status: "malformed",
|
||||
reason: expect.stringContaining("not UTF-8"),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it.each([0, 129])(
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
machoDependencyCommand,
|
||||
readMachoDylibCommandFacts,
|
||||
} from "../domain/apple/machoDylibCommand.js";
|
||||
import { readMachoSectionHeader } from "../domain/apple/machoSectionHeader.js";
|
||||
import { readMachoSegmentHeader } from "../domain/apple/machoSegmentHeader.js";
|
||||
import {
|
||||
@@ -328,9 +332,6 @@ const collectSegment = (
|
||||
const LC_DYLD_INFO = 0x22;
|
||||
const LC_DYLD_INFO_ONLY = 0x80000022;
|
||||
const LC_DYLD_CHAINED_FIXUPS = 0x80000034;
|
||||
const DYLIB_COMMANDS: readonly number[] = [
|
||||
0xc, 0x80000018, 0x8000001f, 0x20, 0x80000023,
|
||||
];
|
||||
|
||||
/** Record fixup data ranges and dylib install names from one load command. */
|
||||
const collectFixupCommand = (
|
||||
@@ -360,16 +361,27 @@ const collectFixupCommand = (
|
||||
const bound = range(at);
|
||||
if (bound.size > 0) state.binds.push({ ...bound, stream });
|
||||
}
|
||||
} else if (DYLIB_COMMANDS.includes(kind)) {
|
||||
if (size < 24) throw new RangeError("Truncated dylib command");
|
||||
const nameOffset = bytes.readUInt32LE(cursor + 8);
|
||||
if (nameOffset < 24 || nameOffset >= size)
|
||||
} else if (machoDependencyCommand(kind) !== null) {
|
||||
const parsed = readMachoDylibCommandFacts(
|
||||
bytes.subarray(cursor, cursor + size),
|
||||
true,
|
||||
);
|
||||
if (!parsed.ok) {
|
||||
if (parsed.error._tag === "InvalidMachoDylibCommand")
|
||||
throw new RangeError("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");
|
||||
const end = bytes.indexOf(0, cursor + nameOffset);
|
||||
if (end < 0 || end >= cursor + size)
|
||||
throw new RangeError("Unterminated dylib install name");
|
||||
}
|
||||
const { nameBytes } = parsed.value;
|
||||
state.dylibs.push(
|
||||
decodeMachoName(bytes.subarray(cursor + nameOffset, end)),
|
||||
decodeMachoName(
|
||||
Buffer.from(
|
||||
nameBytes.buffer,
|
||||
nameBytes.byteOffset,
|
||||
nameBytes.byteLength,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user