fix(apple): keep the recorded image name and frame symbol offset of IPS reports (#7059)

* fix(apple): keep the recorded image name and frame symbol offset of IPS reports

Apple's IPS format documents usedImages[].name and frames[].symbolLocation, and docs/apple-ips.md says documented body fields are reproduced, but the parser shapes did not list either member. They were syntax-checked and discarded without an unlisted_keys entry, so a symbolicated frame lost its +offset and every image its binary name. Both are now kept: images[].name and frames[].symbol_location as a recorded integer, with type mismatches recorded like other fields.

* fix(apple): count frame symbol offsets in the integer text check
This commit is contained in:
Adolanium
2026-10-25 05:18:52 +02:00
committed by GitHub
parent 4ed33efb0c
commit bffe8a52a1
6 changed files with 94 additions and 2 deletions
+3 -1
View File
@@ -29,7 +29,9 @@ body is parsed only when that string is `"309"`. Other types, including `"288"`
stackshots, return no crash result.
`process`, `exception`, `termination`, `faulting_thread`, `threads`, and
`images` reproduce documented body fields when present. Integers keep their
`images` reproduce documented body fields when present, including each image's
`name` and each frame's `symbolLocation` (as `symbol_location`, the byte offset
from the start of `recorded_symbol`). Integers keep their
decimal literals and a hexadecimal form, including arm64e registers above
2^53. Frame roles follow the recorded frame index: row zero of the resolved
faulting thread is `faulting_instruction`, row zero of other threads is
@@ -273,3 +273,46 @@ it.each([
if (!result.ok) expect(result.error._tag).toBe("AnalysisOutputError");
},
);
it.each([
[21, false],
[22, true],
] as const)(
"counts a frame symbol offset against %s UTF-16 integer text bytes",
async (maximum, accepted) => {
const withOffset: AppleIps = {
...report,
threads: [
{
index: 0,
id: null,
triggered: null,
name: null,
queue: null,
frames: [
{
thread_index: 0,
frame_index: 0,
image_index: null,
image_offset: null,
runtime_address: null,
role: "unknown",
inline: null,
recorded_symbol: "main",
symbol_location: { decimal: "12345", hex: "0x3039" },
},
],
state_flavor: null,
registers: [],
},
],
coverage: { ...report.coverage, threads: 1, frames: 1 },
};
const result = await new AppleIpsService({
identity,
inspect: () => Promise.resolve(ok(withOffset)),
}).inspect({ path: report.artifact.path, max_integer_text_bytes: maximum });
expect(result.ok).toBe(accepted);
if (!result.ok) expect(result.error._tag).toBe("AnalysisOutputError");
},
);
+8 -1
View File
@@ -170,6 +170,11 @@ const frameSchema = z.strictObject({
]),
inline: z.boolean().nullable(),
recorded_symbol: z.string().nullable(),
symbol_location: recordedIntegerSchema
.nullable()
.describe(
"Recorded byte offset of the address from the start of recorded_symbol.",
),
});
const threadSchema = z.strictObject({
@@ -192,6 +197,7 @@ const imageSchema = z.strictObject({
size: recordedIntegerSchema.nullable(),
uuid: uuidSchema.nullable(),
uuid_unknown: z.boolean(),
name: z.string().nullable(),
path: z.string().nullable(),
});
@@ -321,7 +327,8 @@ export const appleIpsReportIntegerTextBytes = (report: AppleIps): number => {
for (const frame of thread.frames)
bytes +=
appleIpsIntegerTextBytes(frame.image_offset) +
appleIpsIntegerTextBytes(frame.runtime_address);
appleIpsIntegerTextBytes(frame.runtime_address) +
appleIpsIntegerTextBytes(frame.symbol_location);
}
for (const mismatch of report.coverage.type_mismatches)
bytes += 2 * (mismatch.recorded_integer?.length ?? 0);
+2
View File
@@ -86,6 +86,7 @@ const threadShape = (
"imageIndex",
"imageOffset",
"symbol",
"symbolLocation",
"inline",
),
},
@@ -180,6 +181,7 @@ export const ipsBodyShape = (input: InspectAppleIpsInput): JsonShape => {
"base",
"size",
"uuid",
"name",
"path",
),
},
+33
View File
@@ -402,6 +402,39 @@ it("keeps ReportCrash-shaped producer facts and reports complete coverage", () =
});
});
it("keeps the recorded image name and symbol offset", () => {
const parsed = report({});
expect(parsed.images.map((image) => image.name)).toEqual([
"rea-crash-fixture",
null,
]);
expect(parsed.threads[0]?.frames[0]).toMatchObject({
recorded_symbol: "rea_fixture_fault",
symbol_location: { decimal: "12", hex: "0xc" },
});
expect(parsed.threads[1]?.frames[0]?.symbol_location).toEqual({
decimal: "0",
hex: "0x0",
});
expect(parsed.coverage.status).toBe("complete");
const wrong = parse(
Buffer.from(
appleIpsReport().replace(
'"symbolLocation": 12',
'"symbolLocation": "12"',
),
"utf8",
),
);
expect(wrong.threads[0]?.frames[0]?.symbol_location).toBeNull();
expect(wrong.coverage.type_mismatches).toContainEqual(
expect.objectContaining({
path: "body.threads[0].frames[0].symbolLocation",
expected: "integer",
}),
);
});
it("assigns frame roles by recorded row without inventing inline stack addresses", () => {
const roles = (parsed: ReturnType<typeof parse>) =>
parsed.threads.map((thread) => thread.frames.map((frame) => frame.role));
+5
View File
@@ -222,6 +222,7 @@ const imagesOf = (
size: read.integer(members.size, `${path}.size`),
uuid: uuid.uuid,
uuid_unknown: uuid.unknown || arch === null || arch === "",
name: read.text(members.name, `${path}.name`),
path: read.text(members.path, `${path}.path`),
};
});
@@ -283,6 +284,10 @@ const threadsOf = (
role,
inline,
recorded_symbol: read.text(frameMembers.symbol, `${framePath}.symbol`),
symbol_location: read.integer(
frameMembers.symbolLocation,
`${framePath}.symbolLocation`,
),
};
});
return {