diff --git a/docs/apple-ips.md b/docs/apple-ips.md index e6e260c61f..796256d7f0 100644 --- a/docs/apple-ips.md +++ b/docs/apple-ips.md @@ -31,7 +31,10 @@ stackshots, return no crash result. `process`, `exception`, `termination`, `faulting_thread`, `threads`, and `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 +from the start of `recorded_symbol`). `termination` keeps the terminating +process as `by_proc` and `by_pid` and its `flags`; `process.parent_pid` keeps +`parentPid` next to `parent`; and `metadata` keeps `build_version` and +`platform`. 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 e33ba67cca..e7daf4329a 100644 --- a/src/application/binaryDiagnostics/AppleIpsService.test.ts +++ b/src/application/binaryDiagnostics/AppleIpsService.test.ts @@ -22,6 +22,8 @@ const report: AppleIps = { bundle_id: null, incident_id: null, name: null, + build_version: null, + platform: null, unlisted_keys: [], }, process: { @@ -29,6 +31,7 @@ const report: AppleIps = { path: null, pid: null, parent: null, + parent_pid: null, cpu_type: null, translated: null, os_version: null, @@ -200,19 +203,23 @@ it.each(["threads", "images"] as const)( ); it.each([ - [43, false], - [44, true], + [131, false], + [132, true], ] as const)( "checks repeated integer text against %s UTF-16 bytes", async (maximum, accepted) => { const value = { decimal: "12345", hex: "0x3039" }; const repeated: AppleIps = { ...report, - process: { ...report.process, pid: value }, + metadata: { ...report.metadata, platform: value }, + process: { ...report.process, pid: value, parent_pid: value }, termination: { namespace: null, code: value, indicator: null, + flags: value, + by_pid: value, + by_proc: null, reasons: null, unlisted_keys: [], }, diff --git a/src/domain/native/appleIps.test.ts b/src/domain/native/appleIps.test.ts index aab4c4d692..baf2cae70b 100644 --- a/src/domain/native/appleIps.test.ts +++ b/src/domain/native/appleIps.test.ts @@ -40,6 +40,8 @@ it("rejects extra caller arguments and admits an empty crash inventory", () => { bundle_id: null, incident_id: null, name: null, + build_version: null, + platform: null, unlisted_keys: [], }, process: { @@ -47,6 +49,7 @@ it("rejects extra caller arguments and admits an empty crash inventory", () => { path: null, pid: null, parent: null, + parent_pid: null, cpu_type: null, translated: null, os_version: null, diff --git a/src/domain/native/appleIps.ts b/src/domain/native/appleIps.ts index bd7ffc14ec..b390b8de5a 100644 --- a/src/domain/native/appleIps.ts +++ b/src/domain/native/appleIps.ts @@ -122,6 +122,8 @@ const metadataSchema = z.strictObject({ bundle_id: z.string().nullable(), incident_id: z.string().nullable(), name: z.string().nullable(), + build_version: z.string().nullable(), + platform: recordedIntegerSchema.nullable(), unlisted_keys: z.array(z.string()), }); @@ -141,6 +143,9 @@ const terminationSchema = z namespace: z.string().nullable(), code: recordedIntegerSchema.nullable(), indicator: z.string().nullable(), + flags: recordedIntegerSchema.nullable(), + by_pid: recordedIntegerSchema.nullable(), + by_proc: z.string().nullable(), reasons: z.array(z.string()).nullable(), unlisted_keys: z.array(z.string()), }) @@ -275,6 +280,7 @@ export const appleIpsSchema = z.strictObject({ path: z.string().nullable(), pid: recordedIntegerSchema.nullable(), parent: z.string().nullable(), + parent_pid: recordedIntegerSchema.nullable(), cpu_type: z.string().nullable(), translated: z.boolean().nullable(), os_version: osVersionSchema, @@ -314,8 +320,12 @@ export const appleIpsIntegerTextBytes = ( /** The named report fields own which observed and derived integers are retained. */ export const appleIpsReportIntegerTextBytes = (report: AppleIps): number => { let bytes = + appleIpsIntegerTextBytes(report.metadata.platform) + appleIpsIntegerTextBytes(report.process.pid) + - appleIpsIntegerTextBytes(report.termination?.code ?? null); + appleIpsIntegerTextBytes(report.process.parent_pid) + + appleIpsIntegerTextBytes(report.termination?.code ?? null) + + appleIpsIntegerTextBytes(report.termination?.flags ?? null) + + appleIpsIntegerTextBytes(report.termination?.by_pid ?? null); for (const image of report.images) bytes += appleIpsIntegerTextBytes(image.base) + diff --git a/src/native/apple/AppleIpsJsonShape.ts b/src/native/apple/AppleIpsJsonShape.ts index c4ba323605..da0460d124 100644 --- a/src/native/apple/AppleIpsJsonShape.ts +++ b/src/native/apple/AppleIpsJsonShape.ts @@ -122,7 +122,14 @@ const terminationShape = (): JsonShape => { return { kind: "object", members: { - ...scalarMembers("namespace", "code", "indicator"), + ...scalarMembers( + "namespace", + "code", + "indicator", + "flags", + "byPid", + "byProc", + ), reasons: { kind: "array", items: SCALAR, @@ -150,6 +157,7 @@ export const ipsBodyShape = (input: InspectAppleIpsInput): JsonShape => { "procName", "procPath", "pid", + "parentPid", "parentProc", "cpuType", "translated", @@ -202,6 +210,8 @@ export const IPS_METADATA_SHAPE: JsonShape = { "bundleID", "incident_id", "name", + "build_version", + "platform", ), beforeUnlisted: unlistedKeys, }; diff --git a/src/native/apple/AppleIpsParser.test.ts b/src/native/apple/AppleIpsParser.test.ts index 3dbdd51c20..d337c8e024 100644 --- a/src/native/apple/AppleIpsParser.test.ts +++ b/src/native/apple/AppleIpsParser.test.ts @@ -383,11 +383,7 @@ it("keeps ReportCrash-shaped producer facts and reports complete coverage", () = description: "(Data Abort) byte read Translation fault", }); expect(parsed.exception?.unlisted_keys).toEqual(["rawCodes"]); - expect(parsed.termination?.unlisted_keys).toEqual([ - "flags", - "byProc", - "byPid", - ]); + expect(parsed.termination?.unlisted_keys).toEqual([]); expect(parsed.unlisted_body_keys).toContain("sharedCache"); expect(parsed.unlisted_body_keys).not.toContain("osVersion"); expect(parsed.metadata.unlisted_keys).toContain("slice_uuid"); @@ -402,6 +398,37 @@ it("keeps ReportCrash-shaped producer facts and reports complete coverage", () = }); }); +it("keeps the documented terminating process, parent pid and metadata members", () => { + const parsed = report({}); + expect(parsed.termination).toMatchObject({ + flags: { decimal: "0", hex: "0x0" }, + by_pid: { decimal: "4242", hex: "0x1092" }, + by_proc: "exc handler", + }); + expect(parsed.process.parent_pid).toEqual({ decimal: "100", hex: "0x64" }); + expect(parsed.metadata).toMatchObject({ + build_version: "", + platform: { decimal: "1", hex: "0x1" }, + }); + expect(parsed.metadata.unlisted_keys).not.toContain("build_version"); + expect(parsed.metadata.unlisted_keys).not.toContain("platform"); + expect(parsed.unlisted_body_keys).not.toContain("parentPid"); + expect(parsed.coverage.status).toBe("complete"); + const wrong = parse( + Buffer.from( + appleIpsReport().replace('"byPid": 4242', '"byPid": "4242"'), + "utf8", + ), + ); + expect(wrong.termination?.by_pid).toBeNull(); + expect(wrong.coverage.type_mismatches).toContainEqual( + expect.objectContaining({ + path: "body.termination.byPid", + expected: "integer", + }), + ); +}); + it("keeps the recorded image name and symbol offset", () => { const parsed = report({}); expect(parsed.images.map((image) => image.name)).toEqual([ @@ -511,8 +538,10 @@ it.each(["-1", "4294967296", `1${"0".repeat(1000)}`])( ); it("bounds rejected index text before retaining it", () => { - const bytes = withBody( - '{"faultingThread":4294967296,"threads":[],"usedImages":[]}', + // A bare metadata line: its recorded platform integer would also count toward this allowance. + const bytes = Buffer.from( + '{"bug_type":"309"}\n{"faultingThread":4294967296,"threads":[],"usedImages":[]}', + "utf8", ); expect(() => parseAppleIps(bytes, artifact(bytes), { diff --git a/src/native/apple/AppleIpsParser.ts b/src/native/apple/AppleIpsParser.ts index c8a4687606..e1dc7157ea 100644 --- a/src/native/apple/AppleIpsParser.ts +++ b/src/native/apple/AppleIpsParser.ts @@ -86,6 +86,9 @@ const terminationOf = ( namespace: read.text(members.namespace, "body.termination.namespace"), code: read.integer(members.code, "body.termination.code"), indicator: read.text(members.indicator, "body.termination.indicator"), + flags: read.integer(members.flags, "body.termination.flags"), + by_pid: read.integer(members.byPid, "body.termination.byPid"), + by_proc: read.text(members.byProc, "body.termination.byProc"), reasons: reasonItems === null ? null @@ -353,6 +356,8 @@ const metadataOf = (text: string, read: FieldReader): AppleIps["metadata"] => { bundle_id: read.text(members.bundleID, "metadata.bundleID"), incident_id: read.text(members.incident_id, "metadata.incident_id"), name: read.text(members.name, "metadata.name"), + build_version: read.text(members.build_version, "metadata.build_version"), + platform: read.integer(members.platform, "metadata.platform"), unlisted_keys: [...json.unlisted], }; }; @@ -433,6 +438,7 @@ export const parseAppleIps = ( path: read.text(body.procPath, "body.procPath"), pid: read.integer(body.pid, "body.pid"), parent: read.text(body.parentProc, "body.parentProc"), + parent_pid: read.integer(body.parentPid, "body.parentPid"), cpu_type: read.text(body.cpuType, "body.cpuType"), translated: read.boolean(body.translated, "body.translated"), os_version: osVersionOf(body.osVersion, read),