Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/gpt.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
167 changes: 164 additions & 3 deletions src/gpt.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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");
Expand All @@ -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).
Expand Down
13 changes: 12 additions & 1 deletion src/qdl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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]));
Expand All @@ -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]));
Expand Down