fix(apple): keep the documented terminating process, parent pid and metadata members of IPS reports (#7079)

Apple's crash report JSON documents termination.byPid, byProc and flags, the body's parentPid, and
the metadata line's build_version and platform. The parser shapes did not list them, so they were
reported only by name in unlisted_keys / unlisted_body_keys and their values were dropped: a
watchdog kill no longer said which process ended it, and process kept parentProc without its pid.
#4297 asks for termination members beyond namespace/code/indicator to be retained as recorded, and
docs/apple-ips.md says documented fields are reproduced.

They are now kept: termination.by_proc, termination.by_pid and termination.flags, process.parent_pid
and metadata.build_version / metadata.platform. The integers are recorded integers, so they count
toward max_integer_text_bytes and a type mismatch is recorded like the other fields.
This commit is contained in:
rodricksz4h5 authored and GitHub committed 2026-10-25 13:56:51 +08:00
1 parent cc146042d1
commit 0a2b7af905
7 files changed
+81 -13

No files matched your search

+4 -1
View File
@@ -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
@@ -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: [],
},
+3
View File
@@ -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,
+11 -1
View File
@@ -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) +
+11 -1
View File
@@ -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,
};
+36 -7
View File
@@ -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), {
+6
View File
@@ -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),