fix(apple): preserve modern cache subtype metadata

This commit is contained in:
morluto 2026-10-08 23:26:33 +08:00
parent 8289ede5db
commit 22e6110acb
6 changed files with 95 additions and 3 deletions

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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),

View File

@ -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));

View File

@ -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" },