diff --git a/src/gpt.js b/src/gpt.js index 70bceb5..9db9525 100644 --- a/src/gpt.js +++ b/src/gpt.js @@ -280,6 +280,30 @@ export class GPT { return bestSlot; } + /** + * Swap partition type GUIDs between _a and _b counterparts on this LUN. + * Matches ABL SwitchPtnSlots() / SwapPtnGuid() behavior. + * https://git.codelinaro.org/clo/qcomlt/abl/-/blob/LE.UM.2.3.7/QcomModulePkg/Library/BootLib/PartitionTableUpdate.c#L452-510 + */ + swapSlotGuids() { + const pairs = new Map(); + for (const partEntry of this.#partEntries) { + if (partEntry.type === TYPE_EFI_UNUSED) continue; + const name = partEntry.name; + if (!name.endsWith("_a") && !name.endsWith("_b")) continue; + const baseName = name.slice(0, -2); + if (!pairs.has(baseName)) pairs.set(baseName, {}); + pairs.get(baseName)[name.slice(-1)] = partEntry; + } + for (const slots of pairs.values()) { + if (slots.a && slots.b) { + const temp = slots.a.type; + slots.a.type = slots.b.type; + slots.b.type = temp; + } + } + } + /** * Matches ABL SetActiveSlot() + MarkPtnActive() behavior. * https://git.codelinaro.org/clo/qcomlt/abl/-/blob/LE.UM.2.3.7/QcomModulePkg/Library/BootLib/PartitionTableUpdate.c#L1233-1320 diff --git a/src/gpt.spec.js b/src/gpt.spec.js index bf441a3..3fdf5b5 100644 --- a/src/gpt.spec.js +++ b/src/gpt.spec.js @@ -9,9 +9,30 @@ import { GPT } from "./gpt"; const SECTOR_SIZE = 4096; +/** + * Write a GUID string to a DataView at the given offset (mixed-endian format). + * @param {DataView} view + * @param {number} offset + * @param {string} guid + */ +function writeGuid(view, offset, guid) { + const parts = guid.split("-"); + view.setUint32(offset, Number.parseInt(parts[0], 16), true); + view.setUint16(offset + 4, Number.parseInt(parts[1], 16), true); + view.setUint16(offset + 6, Number.parseInt(parts[2], 16), true); + const clockSeq = Number.parseInt(parts[3], 16); + view.setUint8(offset + 8, (clockSeq >> 8) & 0xFF); + view.setUint8(offset + 9, clockSeq & 0xFF); + const nodeHex = parts[4]; + for (let i = 0; i < 6; i++) { + view.setUint8(offset + 10 + i, Number.parseInt(nodeHex.substring(i * 2, i * 2 + 2), 16)); + } +} + + /** * Build minimal GPT binary data with specified partitions. - * @param {{ name: string, attributes?: bigint }[]} partitions + * @param {{ name: string, type?: string, attributes?: bigint }[]} partitions * @returns {{ header: Uint8Array, entries: Uint8Array }} */ function buildGPTData(partitions) { @@ -26,8 +47,12 @@ function buildGPTData(partitions) { const off = i * entrySize; const p = partitions[i]; - // Type GUID: non-zero = used partition - entriesView.setUint32(off, 0xDEADBEEF, true); + // Type GUID: full string or default non-zero marker + if (p.type) { + writeGuid(entriesView, off, p.type); + } else { + entriesView.setUint32(off, 0xDEADBEEF, true); + } // Unique GUID entriesView.setUint32(off + 16, i + 1, true); @@ -93,6 +118,15 @@ function attrOf(gpt, name) { return gpt.getPartitions().find((p) => p.name === name)?.attributes; } +/** + * Get the type GUID for a partition by name. + * @param {GPT} gpt + * @param {string} name + */ +function typeOf(gpt, name) { + return gpt.getPartitions().find((p) => p.name === name)?.type; +} + // Typical comma device LUN 4 partitions (all starting at zero attributes) const LUN4_PARTITIONS = [ @@ -308,6 +342,14 @@ describe("A/B partition flags", () => { expect(gpt.getActiveSlot()).toBeNull(); }); + test("equal priority tiebreak: first in partition table wins (slot A)", () => { + const gpt = createTestGPT([ + { name: "boot_a", attributes: (3n << 48n) | (1n << 50n) }, + { name: "boot_b", attributes: (3n << 48n) | (1n << 50n) }, + ]); + expect(gpt.getActiveSlot()).toBe("a"); + }); + test("round-trips with setActiveSlot", () => { const gpt = createTestGPT(LUN4_PARTITIONS); gpt.setActiveSlot("a"); @@ -320,6 +362,125 @@ describe("A/B partition flags", () => { }); + describe("swapSlotGuids", () => { + // Real partition type GUIDs from comma device firmware (gpt_main_*.img) + const GUID_INACTIVE = "77036cd4-03d5-42bb-8ed1-37e5a88baa34"; + const GUID_BOOT = "20117f86-e985-4357-b9ee-374bc1d8487d"; + const GUID_AOP = "d69e90a5-4cab-0071-f6df-ab977f141a7f"; + const GUID_TZ = "a053aa7f-40b8-4b1c-ba08-2f68ac71a4f4"; + const GUID_ABL = "bd6928a1-4ce0-a038-4f3a-1495e3eddffb"; + const GUID_SYSTEM = "97d7b011-54da-4835-b3c4-917ad6e73d74"; + const GUID_MISC = "82acc91f-357c-4a68-9c8f-689e1b1a23a1"; + + test("swaps type GUIDs between _a and _b counterparts", () => { + const gpt = createTestGPT([ + { name: "boot_a", type: GUID_BOOT }, + { name: "boot_b", type: GUID_INACTIVE }, + { name: "aop_a", type: GUID_AOP }, + { name: "aop_b", type: GUID_INACTIVE }, + { name: "tz_a", type: GUID_TZ }, + { name: "tz_b", type: GUID_INACTIVE }, + ]); + + gpt.swapSlotGuids(); + + expect(typeOf(gpt, "boot_a")).toBe(GUID_INACTIVE); + expect(typeOf(gpt, "boot_b")).toBe(GUID_BOOT); + expect(typeOf(gpt, "aop_a")).toBe(GUID_INACTIVE); + expect(typeOf(gpt, "aop_b")).toBe(GUID_AOP); + expect(typeOf(gpt, "tz_a")).toBe(GUID_INACTIVE); + expect(typeOf(gpt, "tz_b")).toBe(GUID_TZ); + }); + + test("double swap restores original GUIDs", () => { + const gpt = createTestGPT([ + { name: "boot_a", type: GUID_BOOT }, + { name: "boot_b", type: GUID_INACTIVE }, + { name: "abl_a", type: GUID_ABL }, + { name: "abl_b", type: GUID_INACTIVE }, + ]); + gpt.swapSlotGuids(); + gpt.swapSlotGuids(); + expect(typeOf(gpt, "boot_a")).toBe(GUID_BOOT); + expect(typeOf(gpt, "boot_b")).toBe(GUID_INACTIVE); + expect(typeOf(gpt, "abl_a")).toBe(GUID_ABL); + expect(typeOf(gpt, "abl_b")).toBe(GUID_INACTIVE); + }); + + test("skips non-slotted partitions", () => { + const gpt = createTestGPT([ + { name: "boot_a", type: GUID_BOOT }, + { name: "boot_b", type: GUID_INACTIVE }, + { name: "misc", type: GUID_MISC }, + ]); + gpt.swapSlotGuids(); + expect(typeOf(gpt, "misc")).toBe(GUID_MISC); + }); + + test("skips unmatched partitions (only _a, no _b on this LUN)", () => { + // e.g. LUN 0 has system_a/system_b but boot_a is on LUN 4 + const gpt = createTestGPT([ + { name: "system_a", type: GUID_SYSTEM }, + { name: "system_b", type: GUID_INACTIVE }, + { name: "boot_a", type: GUID_BOOT }, + ]); + gpt.swapSlotGuids(); + // system swapped, boot_a unchanged (no boot_b on this LUN) + expect(typeOf(gpt, "system_a")).toBe(GUID_INACTIVE); + expect(typeOf(gpt, "system_b")).toBe(GUID_SYSTEM); + expect(typeOf(gpt, "boot_a")).toBe(GUID_BOOT); + }); + + test("does not affect partition attributes", () => { + const gpt = createTestGPT([ + { name: "boot_a", type: GUID_BOOT, attributes: 0x003f000000000000n }, + { name: "boot_b", type: GUID_INACTIVE, attributes: 0x0002000000000000n }, + { name: "aop_a", type: GUID_AOP, attributes: 0x1004000000000000n }, + { name: "aop_b", type: GUID_INACTIVE, attributes: 0x1000000000000000n }, + ]); + gpt.swapSlotGuids(); + expect(attrOf(gpt, "boot_a")).toBe("0x003f000000000000"); + expect(attrOf(gpt, "boot_b")).toBe("0x0002000000000000"); + expect(attrOf(gpt, "aop_a")).toBe("0x1004000000000000"); + expect(attrOf(gpt, "aop_b")).toBe("0x1000000000000000"); + }); + + test("factory LUN 4: swap activates slot B GUIDs", () => { + // Factory state: slot A has real GUIDs, slot B has inactive placeholder + const gpt = createTestGPT([ + { name: "boot_a", type: GUID_BOOT, attributes: 0x003f000000000000n }, + { name: "boot_b", type: GUID_INACTIVE, attributes: 0x0002000000000000n }, + { name: "aop_a", type: GUID_AOP, attributes: 0x1004000000000000n }, + { name: "aop_b", type: GUID_INACTIVE, attributes: 0x0000000000000000n }, + { name: "abl_a", type: GUID_ABL, attributes: 0x1004000000000000n }, + { name: "abl_b", type: GUID_INACTIVE, attributes: 0x1000000000000000n }, + ]); + + gpt.swapSlotGuids(); + + // Slot B now has real GUIDs, slot A has placeholder + expect(typeOf(gpt, "boot_a")).toBe(GUID_INACTIVE); + expect(typeOf(gpt, "boot_b")).toBe(GUID_BOOT); + expect(typeOf(gpt, "aop_a")).toBe(GUID_INACTIVE); + expect(typeOf(gpt, "aop_b")).toBe(GUID_AOP); + expect(typeOf(gpt, "abl_a")).toBe(GUID_INACTIVE); + expect(typeOf(gpt, "abl_b")).toBe(GUID_ABL); + }); + + test("factory LUN 0: swap activates system_b GUID", () => { + const gpt = createTestGPT([ + { name: "system_a", type: GUID_SYSTEM, attributes: 0x0004000000000000n }, + { name: "system_b", type: GUID_INACTIVE, attributes: 0x0000000000000000n }, + ]); + + gpt.swapSlotGuids(); + + expect(typeOf(gpt, "system_a")).toBe(GUID_INACTIVE); + expect(typeOf(gpt, "system_b")).toBe(GUID_SYSTEM); + }); + }); + + describe("real device attribute values", () => { // Golden test vectors from live device readings captured during bug investigation. // Bytes 56-63 contain garbage from old buggy tools (0x11, 0x10, 0x21, 0x20). diff --git a/src/qdl.js b/src/qdl.js index d432760..fa69d49 100644 --- a/src/qdl.js +++ b/src/qdl.js @@ -107,7 +107,7 @@ export class qdlDevice { return backupGpt; } if (!partEntriesConsistency) { - logger.warn(`LUN ${lun}: Primary and backup GPT part entries are inconsistent, using primary`); + logger.debug(`LUN ${lun}: Primary and backup GPT part entries are inconsistent, using primary`); // TODO: create backup from primary } return primaryGpt; @@ -354,10 +354,20 @@ export class qdlDevice { async setActiveSlot(slot) { if (slot !== "a" && slot !== "b") throw new Error("Invalid slot"); + // Detect current slot to decide if GUID swap is needed (ABL SwitchPtnSlots) + let currentSlot = null; + for (const lun of this.firehose.luns) { + const gpt = await this.getGpt(lun, 1n); + currentSlot = gpt.getActiveSlot(); + if (currentSlot !== null) break; + } + const shouldSwapGuids = currentSlot !== null && currentSlot !== slot; + for (const lun of this.firehose.luns) { // Update primary GPT const primaryGpt = await this.getGpt(lun, 1n); primaryGpt.setActiveSlot(slot); + if (shouldSwapGuids) primaryGpt.swapSlotGuids(); const primaryPartEntries = primaryGpt.buildPartEntries(); await this.firehose.cmdProgram(lun, primaryGpt.partEntriesStartLba, new Blob([primaryPartEntries])); @@ -367,6 +377,7 @@ export class qdlDevice { // Update backup GPT const backupGpt = await this.getGpt(lun, primaryGpt.alternateLba); backupGpt.setActiveSlot(slot); + if (shouldSwapGuids) backupGpt.swapSlotGuids(); const backupPartEntries = backupGpt.buildPartEntries(); await this.firehose.cmdProgram(lun, backupGpt.partEntriesStartLba, new Blob([backupPartEntries]));