From 1889ac15329dd2019c3ba7f7755ed727e589bd47 Mon Sep 17 00:00:00 2001 From: Adolanium <94890352+Adolanium@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:35:01 +0300 Subject: [PATCH] fix(apple): keep the nested Rosetta registers of translated crash reports A translated process records Rosetta's registers in a nested threadState.rosetta object. The parser read it as one register, so every valid report from a Rosetta process came back partial with a threadState.rosetta.value mismatch, and the nested registers were dropped. Each nested member is now reported as rosetta. and counts toward the register budget. --- docs/apple-ips.md | 3 ++ src/native/apple/AppleIpsJsonShape.ts | 7 ++++ src/native/apple/AppleIpsParser.test.ts | 48 ++++++++++++++++++++++--- src/native/apple/AppleIpsParser.ts | 14 ++++++++ 4 files changed, 68 insertions(+), 4 deletions(-) diff --git a/docs/apple-ips.md b/docs/apple-ips.md index fa5fbd62d6..78821d881f 100644 --- a/docs/apple-ips.md +++ b/docs/apple-ips.md @@ -38,6 +38,9 @@ address uses that row's own image base and offset. The recorded `inline` flag does not supply a missing offset or authorize borrowing another row's address. When `faultingThread` is missing or does not resolve to a recorded thread object, row-zero roles are `unknown`; the recorded lookup remains available. +A translated process records Rosetta's registers in a nested +`threadState.rosetta` object; each one is reported as `rosetta.` and +counts toward the register budget. `process.os_version` keeps the producer's dictionary (`train`, `build`, `release_type`, `is_embedded`) or the older string form. Image UUIDs are uppercase 8-4-4-4-12; an all-zero UUID or a missing `arch` is `uuid_unknown`. Image `source` letters keep their diff --git a/src/native/apple/AppleIpsJsonShape.ts b/src/native/apple/AppleIpsJsonShape.ts index 5ae8b57d52..014834f123 100644 --- a/src/native/apple/AppleIpsJsonShape.ts +++ b/src/native/apple/AppleIpsJsonShape.ts @@ -96,6 +96,13 @@ const threadShape = ( members: { flavor: SCALAR, x: { kind: "array", items: register, beforeItem: take.registers }, + // Rosetta-translated processes nest their translator registers. + rosetta: { + kind: "object", + members: {}, + rest: register, + beforeRest: take.registers, + }, }, rest: register, beforeRest: take.registers, diff --git a/src/native/apple/AppleIpsParser.test.ts b/src/native/apple/AppleIpsParser.test.ts index 487b5cd573..6a8b12dbaa 100644 --- a/src/native/apple/AppleIpsParser.test.ts +++ b/src/native/apple/AppleIpsParser.test.ts @@ -500,6 +500,46 @@ it("reports wrong-type collections as partial instead of empty and complete", () expect(empty.coverage.status).toBe("complete"); }); +it("keeps the nested Rosetta registers of a translated process", () => { + const parsed = parse( + withBody(`{ + "translated": true, + "threads": [ + { + "threadState": { + "flavor": "x86_THREAD_STATE", + "rip": { "value": 4294983680 }, + "rosetta": { "tmp0": { "value": 7 }, "tmp1": 8 } + } + } + ], + "usedImages": [] + }`), + ); + expect(parsed.threads[0]?.registers).toEqual([ + { + name: "rip", + value: { decimal: "4294983680", hex: "0x100004000" }, + description: null, + }, + { + name: "rosetta.tmp0", + value: { decimal: "7", hex: "0x7" }, + description: null, + }, + { + name: "rosetta.tmp1", + value: { decimal: "8", hex: "0x8" }, + description: null, + }, + ]); + expect(parsed.coverage).toMatchObject({ + status: "complete", + registers: 3, + type_mismatches: [], + }); +}); + it("records wrong-type rows, scalars and registers with their source path", () => { const parsed = parse( withBody(`{ @@ -511,7 +551,7 @@ it("records wrong-type rows, scalars and registers with their source path", () = 7, { "frames": [{ "imageIndex": 99999999999, "imageOffset": "8" }, []], - "threadState": { "flavor": 1, "x": [1, "two", { "value": null }, {}], "rosetta": { "tmp0": 1 } } + "threadState": { "flavor": 1, "x": [1, "two", { "value": null }, {}], "rosetta": { "tmp0": "one" } } } ], "usedImages": [null, { "base": 1.0 }] @@ -567,9 +607,9 @@ it("records wrong-type rows, scalars and registers with their source path", () = observed: "absent", }, { - path: "body.threads[1].threadState.rosetta.value", - expected: "integer", - observed: "absent", + path: "body.threads[1].threadState.rosetta.tmp0", + expected: "integer or object with integer value", + observed: "string", }, { path: "body.threads[1].threadState.flavor", diff --git a/src/native/apple/AppleIpsParser.ts b/src/native/apple/AppleIpsParser.ts index 359cb14170..488ad90ab0 100644 --- a/src/native/apple/AppleIpsParser.ts +++ b/src/native/apple/AppleIpsParser.ts @@ -175,6 +175,20 @@ const threadStateOf = ( }); for (const [key, entry] of Object.entries(members)) { if (key === "x" || key === "flavor") continue; + if (key === "rosetta") { + // A translated process records Rosetta's registers as a nested state. + const rosetta = read.object(entry, `${path}.rosetta`); + for (const [name, nested] of Object.entries(rosetta?.members ?? {})) { + const register = registerValue( + `rosetta.${name}`, + nested, + `${path}.rosetta.${name}`, + read, + ); + if (register !== null) registers.push(register); + } + continue; + } const register = registerValue(key, entry, `${path}.${key}`, read); if (register !== null) registers.push(register); }