From bffe8a52a1c4438c01ae932776423453c26ca906 Mon Sep 17 00:00:00 2001 From: Adolanium <94890352+Adolanium@users.noreply.github.com> Date: Sun, 25 Oct 2026 05:18:52 +0200 Subject: [PATCH] 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 --- docs/apple-ips.md | 4 +- .../binaryDiagnostics/AppleIpsService.test.ts | 43 +++++++++++++++++++ src/domain/native/appleIps.ts | 9 +++- src/native/apple/AppleIpsJsonShape.ts | 2 + src/native/apple/AppleIpsParser.test.ts | 33 ++++++++++++++ src/native/apple/AppleIpsParser.ts | 5 +++ 6 files changed, 94 insertions(+), 2 deletions(-) diff --git a/docs/apple-ips.md b/docs/apple-ips.md index 73cc8d204d..e6e260c61f 100644 --- a/docs/apple-ips.md +++ b/docs/apple-ips.md @@ -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 diff --git a/src/application/binaryDiagnostics/AppleIpsService.test.ts b/src/application/binaryDiagnostics/AppleIpsService.test.ts index da84cdcb29..e33ba67cca 100644 --- a/src/application/binaryDiagnostics/AppleIpsService.test.ts +++ b/src/application/binaryDiagnostics/AppleIpsService.test.ts @@ -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"); + }, +); diff --git a/src/domain/native/appleIps.ts b/src/domain/native/appleIps.ts index 057cc37425..bd7ffc14ec 100644 --- a/src/domain/native/appleIps.ts +++ b/src/domain/native/appleIps.ts @@ -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); diff --git a/src/native/apple/AppleIpsJsonShape.ts b/src/native/apple/AppleIpsJsonShape.ts index 014834f123..c4ba323605 100644 --- a/src/native/apple/AppleIpsJsonShape.ts +++ b/src/native/apple/AppleIpsJsonShape.ts @@ -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", ), }, diff --git a/src/native/apple/AppleIpsParser.test.ts b/src/native/apple/AppleIpsParser.test.ts index ef1ae9996b..3dbdd51c20 100644 --- a/src/native/apple/AppleIpsParser.test.ts +++ b/src/native/apple/AppleIpsParser.test.ts @@ -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) => parsed.threads.map((thread) => thread.frames.map((frame) => frame.role)); diff --git a/src/native/apple/AppleIpsParser.ts b/src/native/apple/AppleIpsParser.ts index 9bf8e656ec..c8a4687606 100644 --- a/src/native/apple/AppleIpsParser.ts +++ b/src/native/apple/AppleIpsParser.ts @@ -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 {