fix(apple): preserve modern cache subtype metadata
This commit is contained in:
parent
8289ede5db
commit
22e6110acb
|
|
@ -228,7 +228,8 @@ rea inspect-dyld-shared-cache /System/Volumes/Preboot/Cryptexes/OS/System/Librar
|
|||
The result reports:
|
||||
|
||||
- the magic, architecture, UUID, platform and OS version, any alternate platform
|
||||
(Mac Catalyst), cache type, shared region and maximum slide;
|
||||
(Mac Catalyst), cache type, the development/production subtype for multi-caches
|
||||
(including its raw ID when unknown), shared region and maximum slide;
|
||||
- every VM mapping, with the file that holds it;
|
||||
- every subcache by file suffix, with its UUID checked against the main cache.
|
||||
A missing or mismatched subcache makes coverage `partial`, and images mapped
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { access, mkdtemp, realpath, rm } from "node:fs/promises";
|
||||
import { access, mkdtemp, open, realpath, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
|
@ -120,6 +120,33 @@ try {
|
|||
assert.equal(inspected.images_total, expected.size);
|
||||
assert.equal(inspected.coverage.status, "complete");
|
||||
assert.equal(inspected.inspected_images[0].status, "parsed");
|
||||
// Check the producer header directly, independently of REA's header projection.
|
||||
const handle = await open(cachePath, "r");
|
||||
try {
|
||||
const header = Buffer.alloc(0x1cc);
|
||||
const { bytesRead } = await handle.read(header, 0, header.length, 0);
|
||||
const multi = header.readBigUInt64LE(0x68) === 2n;
|
||||
const hasSubtype =
|
||||
bytesRead === header.length && header.readUInt32LE(0x10) >= header.length;
|
||||
const subtype = multi && hasSubtype ? header.readUInt32LE(0x1c8) : null;
|
||||
assert.deepEqual(
|
||||
inspected.cache_subtype,
|
||||
subtype === null
|
||||
? null
|
||||
: {
|
||||
id: subtype,
|
||||
name:
|
||||
subtype === 0
|
||||
? "development"
|
||||
: subtype === 1
|
||||
? "production"
|
||||
: null,
|
||||
},
|
||||
"multi-cache subtype differs from the producer header",
|
||||
);
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
await withArtifactMcp(null, async (client) => {
|
||||
const viaMcp = await artifactMcpResult(
|
||||
client,
|
||||
|
|
@ -181,6 +208,8 @@ try {
|
|||
architecture: inspected.architecture,
|
||||
os_version: inspected.os_version,
|
||||
images: inspected.images_total,
|
||||
type: inspected.cache_type,
|
||||
subtype: inspected.cache_subtype,
|
||||
},
|
||||
images_matching_dyld_info: compared,
|
||||
trace_shared_cache_edges: cached.length,
|
||||
|
|
|
|||
|
|
@ -571,6 +571,7 @@ export class DyldSharedCache {
|
|||
const cacheType = has(parsed, FIELD.cacheType, 8)
|
||||
? Number(header.readBigUInt64LE(FIELD.cacheType))
|
||||
: null;
|
||||
const cacheSubType = cacheType === 2 ? u32(FIELD.cacheSubType) : null;
|
||||
const symbols = has(parsed, FIELD.symbolFileUuid, 16)
|
||||
? header.subarray(FIELD.symbolFileUuid, FIELD.symbolFileUuid + 16)
|
||||
: undefined;
|
||||
|
|
@ -606,6 +607,18 @@ export class DyldSharedCache {
|
|||
cacheType,
|
||||
has(parsed, FIELD.cacheSubType, 4),
|
||||
),
|
||||
cache_subtype:
|
||||
cacheSubType !== null
|
||||
? {
|
||||
id: cacheSubType,
|
||||
name:
|
||||
cacheSubType === 0
|
||||
? "development"
|
||||
: cacheSubType === 1
|
||||
? "production"
|
||||
: null,
|
||||
}
|
||||
: null,
|
||||
shared_region:
|
||||
u64(FIELD.sharedRegionStart) === null
|
||||
? null
|
||||
|
|
|
|||
|
|
@ -45,6 +45,16 @@ const headerSchema = z.object({
|
|||
alt_platform: applePlatformSchema.nullable(),
|
||||
alt_os_version: z.string().nullable(),
|
||||
cache_type: z.enum(["development", "production", "multi-cache"]).nullable(),
|
||||
cache_subtype: z
|
||||
.object({
|
||||
id: z.number().int().nonnegative(),
|
||||
name: z.enum(["development", "production"]).nullable(),
|
||||
})
|
||||
.nullable()
|
||||
.default(null)
|
||||
.describe(
|
||||
"Multi-cache subtype from the producer header: 0 is development, 1 is production; unknown values retain their ID with a null name. Null when absent or not applicable.",
|
||||
),
|
||||
shared_region: z.object({ start: hexSchema, size: hexSchema }).nullable(),
|
||||
max_slide: hexSchema.nullable(),
|
||||
mappings: z.array(mappingSchema),
|
||||
|
|
|
|||
|
|
@ -174,6 +174,40 @@ const IMAGES = [
|
|||
},
|
||||
];
|
||||
|
||||
it.each([
|
||||
{ type: 2, subtype: 0, expected: { id: 0, name: "development" } },
|
||||
{ type: 2, subtype: 1, expected: { id: 1, name: "production" } },
|
||||
{ type: 2, subtype: 9, expected: { id: 9, name: null } },
|
||||
{ type: 0, subtype: 1, expected: null },
|
||||
{ type: 1, subtype: 0, expected: null },
|
||||
])(
|
||||
"preserves applicable multi-cache subtype $subtype for type $type",
|
||||
async ({ type, subtype, expected }) => {
|
||||
const fixture = dyldCacheFixture(IMAGES);
|
||||
const main = Buffer.from(fixture.main);
|
||||
main.writeBigUInt64LE(BigInt(type), 0x68);
|
||||
main.writeUInt32LE(subtype, 0x1c8);
|
||||
const path = await writeCache({ ...fixture, main });
|
||||
const result = await inspectDyldSharedCache({ cache_path: path });
|
||||
if (!result.ok) throw result.error;
|
||||
const cache = dyldSharedCacheResultSchema.parse(result.value);
|
||||
expect(cache.cache_type).toBe(
|
||||
type === 2 ? "multi-cache" : type === 0 ? "development" : "production",
|
||||
);
|
||||
expect(cache.cache_subtype).toEqual(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps a legacy multi-cache subtype unknown when its header lacks the field", async () => {
|
||||
const fixture = dyldCacheFixture([], { legacy: true });
|
||||
const main = Buffer.from(fixture.main);
|
||||
main.writeBigUInt64LE(2n, 0x68);
|
||||
const path = await writeCache({ ...fixture, main });
|
||||
const result = await inspectDyldSharedCache({ cache_path: path });
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.cache_subtype).toBeNull();
|
||||
});
|
||||
|
||||
describe("dyld shared cache inspection", () => {
|
||||
it("reads the header, subcaches, image list and cached load commands", async () => {
|
||||
const path = await writeCache(dyldCacheFixture(IMAGES));
|
||||
|
|
|
|||
|
|
@ -44,7 +44,10 @@ it("inspects a dyld shared cache through MCP without a target", async () => {
|
|||
}),
|
||||
},
|
||||
]);
|
||||
await writeFile(path, fixture.main);
|
||||
const main = Buffer.from(fixture.main);
|
||||
main.writeBigUInt64LE(2n, 0x68);
|
||||
main.writeUInt32LE(1, 0x1c8);
|
||||
await writeFile(path, main);
|
||||
|
||||
const tools = await client.listTools();
|
||||
expect(tools.tools.map(({ name }) => name)).toContain(
|
||||
|
|
@ -62,6 +65,8 @@ it("inspects a dyld shared cache through MCP without a target", async () => {
|
|||
);
|
||||
expect(result).toMatchObject({
|
||||
architecture: "arm64e",
|
||||
cache_type: "multi-cache",
|
||||
cache_subtype: { id: 1, name: "production" },
|
||||
images_total: 1,
|
||||
inspected_images: [
|
||||
{ path: "/usr/lib/libSystem.B.dylib", status: "parsed" },
|
||||
|
|
|
|||
Loading…
Reference in New Issue