From def29c759a9e8d1bd5d3fc50caf913ad8edb6350 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 05:59:37 +0000 Subject: [PATCH 1/6] spi: apple-qspi: admit verified NVRAM writes on the J700 asahi-bless cannot select a one-time boot volume on the MacBook Neo (J700). The T8140 path admitted programs and erases inside the nvram partition with no runtime opt-in and no readback, while the J613 and J713 go through the read-only, opt-in, verified policy. Writes still never reached the flash, because the partition was read-only in MTD. Put the J700 on the same policy. The controller starts read-only and root opts in at runtime through the read_only parameter. A write needs the J700 board and the controller at 0x385110000. Programs are single-wire, three-byte, within one 256-byte page; erases are aligned 4 KiB sectors; both must lie inside the nvram partition. Every mutation checks the JEDEC ID and status first, is verified by readback, and a fault latches the driver read-only. The J613/J713 partition is pinned at 0x700000. The J700's comes from its device tree (0x24000, size 0x1d0000), so the range check now takes the partition bounds, and a DT window must fit in three-byte addressing. The J700 flash is a W25Q16JV (JEDEC ef6515). iBoot leaves BP0 set, which protects the top 64 KiB, so the J700 tolerates the block-protect bits as found: the driver never changes them, the flash ignores writes in that range, and readback then latches read-only. CMP, suspend, WPS, BUSY and WEL are still refused. T8140 data reads keep their multi-line pass-through. KUnit and the host harness now cover the shared policy at the J700 bounds, the J700 board gate and the block-protect tolerance. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/spi/Kconfig | 16 +-- drivers/spi/spi-apple-qspi-policy-test.c | 128 +++++++++++++---------- drivers/spi/spi-apple-qspi-policy.h | 95 ++++++----------- drivers/spi/spi-apple-qspi.c | 90 ++++++++-------- tools/asahi/qspi-nvram-tests.py | 55 +++++----- 5 files changed, 186 insertions(+), 198 deletions(-) diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig index ad875e80d8ba2..8b53e2cf36b8d 100644 --- a/drivers/spi/Kconfig +++ b/drivers/spi/Kconfig @@ -148,20 +148,20 @@ config SPI_APPLE_QSPI Unlike the ordinary Apple SPIMC block, QSPIMC accepts packed SPI memory descriptors and is used for the boot NOR on T8140 systems. - T8122/T8132 controllers default to read-only access. Only the pinned - J613 NVRAM layout admits bounded writes after a runtime opt-in. - On T8140 only the nvram partition accepts programs and erases; the - rest of the boot NOR is read-only. + The boot NOR defaults to read-only access. On the J613, J713 and + J700 a runtime opt-in admits bounded, verified page programs and + 4 KiB erases inside the nvram partition only; the rest of the boot + NOR stays read-only. config SPI_APPLE_QSPI_KUNIT_TEST - tristate "KUnit tests for the Apple QSPIMC boot NOR write window" if !KUNIT_ALL_TESTS + tristate "KUnit tests for the Apple QSPIMC NVRAM write policy" if !KUNIT_ALL_TESTS depends on KUNIT depends on SPI_APPLE_QSPI || COMPILE_TEST default KUNIT_ALL_TESTS help - Tests which T8140 boot NOR requests the QSPIMC driver admits: write - enable control, and programs and erases inside the nvram partition - only. No hardware is accessed. + Tests which boot NOR requests the QSPIMC driver admits: write + enable control, and page programs and 4 KiB erases inside the + nvram partition only. No hardware is accessed. config SPI_AR934X tristate "Qualcomm Atheros AR934X/QCA95XX SPI controller driver" diff --git a/drivers/spi/spi-apple-qspi-policy-test.c b/drivers/spi/spi-apple-qspi-policy-test.c index 9ec58d587c60f..b1ecbdbe4e26e 100644 --- a/drivers/spi/spi-apple-qspi-policy-test.c +++ b/drivers/spi/spi-apple-qspi-policy-test.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0 /* - * KUnit tests for the T8140 boot NOR write window of the Apple QSPIMC driver. + * KUnit tests for the Apple QSPIMC NVRAM mutation policy, at the J700 and + * J613/J713 nvram partitions. */ #include @@ -14,59 +15,57 @@ static int nv(unsigned int opcode, unsigned int addr_bytes, unsigned long long addr, unsigned int len, int data_out) { - return apple_qspi_window_allowed(opcode, addr_bytes, addr, len, - data_out, NV_START, NV_END); + return apple_qspi_mutation_allowed(opcode, addr_bytes, addr, len, + data_out, NV_START, NV_END); } -static void qspi_window_write_enable(struct kunit *test) +static void qspi_policy_write_enable(struct kunit *test) { KUNIT_EXPECT_TRUE(test, nv(0x06, 0, 0, 0, 0)); KUNIT_EXPECT_TRUE(test, nv(0x04, 0, 0, 0, 0)); KUNIT_EXPECT_FALSE(test, nv(0x06, 3, NV_START, 0, 0)); KUNIT_EXPECT_FALSE(test, nv(0x06, 0, 0, 1, 1)); - /* Write-enable control needs no window. */ - KUNIT_EXPECT_TRUE(test, apple_qspi_window_allowed(0x06, 0, 0, 0, 0, 0, 0)); + /* Write-enable control is virtual and needs no window. */ + KUNIT_EXPECT_TRUE(test, apple_qspi_mutation_allowed(0x06, 0, 0, 0, 0, 0, 0)); } -static void qspi_window_program(struct kunit *test) +static void qspi_policy_program(struct kunit *test) { KUNIT_EXPECT_TRUE(test, nv(0x02, 3, NV_START, 256, 1)); KUNIT_EXPECT_TRUE(test, nv(0x02, 3, NV_START + 0x1234, 1, 1)); KUNIT_EXPECT_TRUE(test, nv(0x02, 3, NV_END - 256, 256, 1)); - KUNIT_EXPECT_TRUE(test, nv(0x12, 4, NV_START, 256, 1)); - /* Any byte outside, or a wrap within a page outside, is refused. */ + /* Any byte outside the partition is refused. */ KUNIT_EXPECT_FALSE(test, nv(0x02, 3, NV_START - 256, 256, 1)); KUNIT_EXPECT_FALSE(test, nv(0x02, 3, NV_START - 1, 2, 1)); KUNIT_EXPECT_FALSE(test, nv(0x02, 3, NV_END, 1, 1)); KUNIT_EXPECT_FALSE(test, nv(0x02, 3, NV_END - 1, 2, 1)); KUNIT_EXPECT_FALSE(test, nv(0x02, 3, 0, 256, 1)); - /* A program needs data out, and at most one 4 KiB block's worth. */ + /* A program stays within one page and needs data out. */ + KUNIT_EXPECT_FALSE(test, nv(0x02, 3, NV_START + 0xff, 2, 1)); + KUNIT_EXPECT_FALSE(test, nv(0x02, 3, NV_START, 257, 1)); KUNIT_EXPECT_FALSE(test, nv(0x02, 3, NV_START, 0, 1)); KUNIT_EXPECT_FALSE(test, nv(0x02, 3, NV_START, 256, 0)); - KUNIT_EXPECT_FALSE(test, nv(0x02, 3, NV_START, 4097, 1)); + /* Four-byte programs are not admitted. */ + KUNIT_EXPECT_FALSE(test, nv(0x12, 4, NV_START, 256, 1)); } -static void qspi_window_erase(struct kunit *test) +static void qspi_policy_erase(struct kunit *test) { KUNIT_EXPECT_TRUE(test, nv(0x20, 3, NV_START, 0, 0)); - KUNIT_EXPECT_TRUE(test, nv(0x20, 3, NV_START + 0x123, 0, 0)); - KUNIT_EXPECT_TRUE(test, nv(0x20, 3, NV_END - 1, 0, 0)); - KUNIT_EXPECT_TRUE(test, nv(0x21, 4, NV_START, 0, 0)); - KUNIT_EXPECT_FALSE(test, nv(0x20, 3, NV_START - 1, 0, 0)); + KUNIT_EXPECT_TRUE(test, nv(0x20, 3, NV_END - 0x1000, 0, 0)); + /* Only aligned 4 KiB sector erases inside the partition. */ + KUNIT_EXPECT_FALSE(test, nv(0x20, 3, NV_START + 0x123, 0, 0)); + KUNIT_EXPECT_FALSE(test, nv(0x20, 3, NV_START - 0x1000, 0, 0)); KUNIT_EXPECT_FALSE(test, nv(0x20, 3, NV_END, 0, 0)); - /* 32 and 64 KiB erases cover the whole aligned block. */ - KUNIT_EXPECT_TRUE(test, nv(0x52, 3, 0x28000, 0, 0)); - KUNIT_EXPECT_FALSE(test, nv(0x52, 3, NV_START, 0, 0)); - KUNIT_EXPECT_TRUE(test, nv(0xd8, 3, 0x30000, 0, 0)); - KUNIT_EXPECT_TRUE(test, nv(0xdc, 4, 0x1e0000, 0, 0)); - KUNIT_EXPECT_FALSE(test, nv(0xd8, 3, NV_START, 0, 0)); - KUNIT_EXPECT_FALSE(test, nv(0xd8, 3, 0x1f0000, 0, 0)); + KUNIT_EXPECT_FALSE(test, nv(0x21, 4, NV_START, 0, 0)); + KUNIT_EXPECT_FALSE(test, nv(0x52, 3, 0x28000, 0, 0)); + KUNIT_EXPECT_FALSE(test, nv(0xd8, 3, 0x30000, 0, 0)); /* An erase carries no data. */ KUNIT_EXPECT_FALSE(test, nv(0x20, 3, NV_START, 1, 0)); KUNIT_EXPECT_FALSE(test, nv(0x20, 3, NV_START, 0, 1)); } -static void qspi_window_refused_commands(struct kunit *test) +static void qspi_policy_refused_commands(struct kunit *test) { static const u8 refused[] = { 0x01, 0x31, 0x11, /* status register writes */ @@ -86,48 +85,67 @@ static void qspi_window_refused_commands(struct kunit *test) } } -static void qspi_window_address_width(struct kunit *test) +static void qspi_policy_address_width(struct kunit *test) { - /* Three address bytes cannot carry an address above 16 MiB. */ - KUNIT_EXPECT_FALSE(test, nv(0x02, 3, (1ULL << 24) + NV_START, 1, 1)); - KUNIT_EXPECT_FALSE(test, nv(0x20, 3, (1ULL << 24) + NV_START, 0, 0)); - KUNIT_EXPECT_FALSE(test, nv(0x12, 4, (1ULL << 32) + NV_START, 1, 1)); KUNIT_EXPECT_FALSE(test, nv(0x02, 2, NV_START, 1, 1)); + KUNIT_EXPECT_FALSE(test, nv(0x02, 4, NV_START, 1, 1)); KUNIT_EXPECT_FALSE(test, nv(0x20, 0, NV_START, 0, 0)); - KUNIT_EXPECT_FALSE(test, nv(0x20, 5, NV_START, 0, 0)); - /* A window that reaches the top of 24-bit addressing. */ - KUNIT_EXPECT_TRUE(test, apple_qspi_window_allowed(0x20, 3, 0xfff000, 0, 0, - 0xff0000, 1ULL << 24)); - /* A program that crosses 16 MiB cannot use three address bytes. */ - KUNIT_EXPECT_FALSE(test, apple_qspi_window_allowed(0x02, 3, 0xffffff, 2, 1, - 0xff0000, 1ULL << 25)); - KUNIT_EXPECT_TRUE(test, apple_qspi_window_allowed(0x12, 4, 0xffffff, 2, 1, - 0xff0000, 1ULL << 25)); + KUNIT_EXPECT_FALSE(test, nv(0x20, 4, NV_START, 0, 0)); + /* Addresses are not truncated to the wire width before the check. */ + KUNIT_EXPECT_FALSE(test, nv(0x02, 3, (1ULL << 24) + NV_START, 1, 1)); + KUNIT_EXPECT_FALSE(test, nv(0x20, 3, (1ULL << 24) + NV_START, 0, 0)); + KUNIT_EXPECT_FALSE(test, nv(0x02, 3, ~0ULL, 1, 1)); +} + +static void qspi_policy_empty(struct kunit *test) +{ + KUNIT_EXPECT_FALSE(test, apple_qspi_mutation_allowed(0x02, 3, 0, 1, 1, 0, 0)); + KUNIT_EXPECT_FALSE(test, apple_qspi_mutation_allowed(0x20, 3, 0, 0, 0, 0, 0)); + KUNIT_EXPECT_FALSE(test, apple_qspi_mutation_allowed(0x20, 3, NV_START, 0, 0, + NV_START, NV_START)); +} + +static void qspi_policy_j613(struct kunit *test) +{ + unsigned long long s = APPLE_QSPI_NVRAM_START, e = APPLE_QSPI_NVRAM_END; + + KUNIT_EXPECT_TRUE(test, apple_qspi_mutation_allowed(0x02, 3, s, 256, 1, s, e)); + KUNIT_EXPECT_TRUE(test, apple_qspi_mutation_allowed(0x20, 3, e - 0x1000, 0, 0, s, e)); + KUNIT_EXPECT_FALSE(test, apple_qspi_mutation_allowed(0x02, 3, s - 1, 1, 1, s, e)); + KUNIT_EXPECT_FALSE(test, apple_qspi_mutation_allowed(0x20, 3, e, 0, 0, s, e)); } -static void qspi_window_empty(struct kunit *test) +static void qspi_policy_status(struct kunit *test) { - KUNIT_EXPECT_FALSE(test, apple_qspi_window_allowed(0x02, 3, 0, 1, 1, 0, 0)); - KUNIT_EXPECT_FALSE(test, apple_qspi_window_allowed(0x20, 3, 0, 0, 0, 0, 0)); - KUNIT_EXPECT_FALSE(test, apple_qspi_window_allowed(0x20, 3, NV_START, 0, 0, - NV_START, NV_START)); + /* BUSY and WEL are never admitted. */ + KUNIT_EXPECT_FALSE(test, apple_qspi_status_allows_write(0x01, 0, 0, 0x1c)); + KUNIT_EXPECT_FALSE(test, apple_qspi_status_allows_write(0x02, 0, 0, 0x1c)); + /* Block protect only where the board allows it. */ + KUNIT_EXPECT_FALSE(test, apple_qspi_status_allows_write(0x04, 0, 0, 0)); + KUNIT_EXPECT_TRUE(test, apple_qspi_status_allows_write(0x04, 0x02, 0, 0x1c)); + /* CMP, SUS and WPS are refused everywhere. */ + KUNIT_EXPECT_FALSE(test, apple_qspi_status_allows_write(0, 0x40, 0, 0x1c)); + KUNIT_EXPECT_FALSE(test, apple_qspi_status_allows_write(0, 0x80, 0, 0x1c)); + KUNIT_EXPECT_FALSE(test, apple_qspi_status_allows_write(0, 0, 0x04, 0x1c)); } -static struct kunit_case apple_qspi_window_cases[] = { - KUNIT_CASE(qspi_window_write_enable), - KUNIT_CASE(qspi_window_program), - KUNIT_CASE(qspi_window_erase), - KUNIT_CASE(qspi_window_refused_commands), - KUNIT_CASE(qspi_window_address_width), - KUNIT_CASE(qspi_window_empty), +static struct kunit_case apple_qspi_policy_cases[] = { + KUNIT_CASE(qspi_policy_write_enable), + KUNIT_CASE(qspi_policy_program), + KUNIT_CASE(qspi_policy_erase), + KUNIT_CASE(qspi_policy_refused_commands), + KUNIT_CASE(qspi_policy_address_width), + KUNIT_CASE(qspi_policy_empty), + KUNIT_CASE(qspi_policy_j613), + KUNIT_CASE(qspi_policy_status), {} }; -static struct kunit_suite apple_qspi_window_suite = { - .name = "apple-qspi-nvram-window", - .test_cases = apple_qspi_window_cases, +static struct kunit_suite apple_qspi_policy_suite = { + .name = "apple-qspi-nvram-policy", + .test_cases = apple_qspi_policy_cases, }; -kunit_test_suite(apple_qspi_window_suite); +kunit_test_suite(apple_qspi_policy_suite); -MODULE_DESCRIPTION("KUnit tests for the Apple QSPIMC boot NOR write window"); +MODULE_DESCRIPTION("KUnit tests for the Apple QSPIMC NVRAM mutation policy"); MODULE_LICENSE("GPL"); diff --git a/drivers/spi/spi-apple-qspi-policy.h b/drivers/spi/spi-apple-qspi-policy.h index 5d788d69f4f8b..12d8ac41dd26c 100644 --- a/drivers/spi/spi-apple-qspi-policy.h +++ b/drivers/spi/spi-apple-qspi-policy.h @@ -2,21 +2,36 @@ #ifndef APPLE_QSPI_POLICY_H #define APPLE_QSPI_POLICY_H +/* The pinned J613/J713 nvram partition. */ #define APPLE_QSPI_NVRAM_START 0x700000ULL #define APPLE_QSPI_NVRAM_END 0x800000ULL #define APPLE_QSPI_PAGE_SIZE 256U #define APPLE_QSPI_ERASE_SIZE 4096U -/* Subtraction avoids overflow; addr is not truncated to wire width first. */ -static inline int apple_qspi_range_allowed(unsigned long long addr, unsigned int len) +/* + * [start, end) is the board's nvram partition. Subtraction avoids overflow; + * addr is not truncated to wire width first. + */ +static inline int apple_qspi_range_allowed(unsigned long long addr, + unsigned int len, unsigned long long start, + unsigned long long end) { - return len && addr >= APPLE_QSPI_NVRAM_START && - addr < APPLE_QSPI_NVRAM_END && len <= APPLE_QSPI_NVRAM_END - addr; + return len && start < end && addr >= start && addr < end && + len <= end - addr; } +/* + * May a write-path request reach the boot NOR? Write enable control is + * virtual and admitted. A single-wire, three-byte page program that stays + * within one 256-byte page, or an aligned 4 KiB sector erase, is admitted + * only when every byte it can change lies inside [start, end). Everything + * else (status and configuration writes, larger erases, chip erase, + * address-mode, reset and OTP commands) is refused. + */ static inline int apple_qspi_mutation_allowed(unsigned int opcode, unsigned int addr_bytes, unsigned long long addr, - unsigned int len, int data_out) + unsigned int len, int data_out, + unsigned long long start, unsigned long long end) { switch (opcode) { case 0x06: /* Virtual WREN, never sent independently. */ @@ -24,76 +39,30 @@ static inline int apple_qspi_mutation_allowed(unsigned int opcode, return !addr_bytes && !len && !data_out; case 0x02: /* PP: prevent page wrap, even if SPI-NOR gets geometry wrong. */ return addr_bytes == 3 && data_out && len <= APPLE_QSPI_PAGE_SIZE && - apple_qspi_range_allowed(addr, len) && + apple_qspi_range_allowed(addr, len, start, end) && len <= APPLE_QSPI_PAGE_SIZE - (addr & (APPLE_QSPI_PAGE_SIZE - 1)); case 0x20: /* SE: hard-code the entire affected 4 KiB range. */ return addr_bytes == 3 && !len && !data_out && !(addr & (APPLE_QSPI_ERASE_SIZE - 1)) && - apple_qspi_range_allowed(addr, APPLE_QSPI_ERASE_SIZE); + apple_qspi_range_allowed(addr, APPLE_QSPI_ERASE_SIZE, + start, end); default: return 0; } } /* - * T8140 boot NOR: may a request that does not read data reach the flash? - * Write-enable control is admitted. A page program, or a 4, 32 or 64 KiB - * erase, is admitted only when every byte it can change lies inside - * [start, end), the nvram partition. A program counts the whole 4 KiB - * blocks it touches, which covers a write that wraps within its page. Status - * and configuration register writes, chip erase, address-mode, reset and OTP - * commands are refused. The address must fit in addr_bytes: the controller - * sends only those bytes, so a wider value would alias a lower address. + * Protection and activity bits must permit a write; JEDEC ID is checked + * separately. BUSY/WEL, CMP/SUS and WPS are always refused. bp_ok names the + * SR1 block-protect bits a board's firmware leaves set at boot: they stay + * unchanged, the flash ignores programs and erases in the range they protect, + * and the driver's readback then latches it read-only. SRP, TB, SEC, QE and + * LB stay unchanged. */ -static inline int apple_qspi_window_allowed(unsigned int opcode, - unsigned int addr_bytes, unsigned long long addr, - unsigned int len, int data_out, - unsigned long long start, unsigned long long end) -{ - unsigned long long block, first, last; - - switch (opcode) { - case 0x06: /* WREN */ - case 0x04: /* WRDI */ - return !addr_bytes && !len && !data_out; - case 0x02: /* PP */ - case 0x12: /* PP_4B */ - if (!data_out || !len || len > APPLE_QSPI_ERASE_SIZE) - return 0; - block = APPLE_QSPI_ERASE_SIZE; - break; - case 0x20: /* BE_4K */ - case 0x21: /* BE_4K_4B */ - block = 0x1000; - break; - case 0x52: /* BE_32K */ - case 0x5c: /* BE_32K_4B */ - block = 0x8000; - break; - case 0xd8: /* SE */ - case 0xdc: /* SE_4B */ - block = 0x10000; - break; - default: - return 0; - } - if (opcode != 0x02 && opcode != 0x12 && (len || data_out)) - return 0; - if ((addr_bytes != 3 && addr_bytes != 4) || addr >> (8 * addr_bytes)) - return 0; - - /* [first, last) are the blocks the flash can change. */ - first = addr & ~(block - 1); - last = ((addr + (len ? len : 1) - 1) | (block - 1)) + 1; - return last <= 1ULL << (8 * addr_bytes) && - start <= first && last <= end; -} - -/* Protection and activity bits must permit a write; JEDEC ID is checked separately. */ static inline int apple_qspi_status_allows_write(unsigned int sr1, - unsigned int sr2, unsigned int sr3) + unsigned int sr2, unsigned int sr3, unsigned int bp_ok) { - /* BUSY/WEL, BP[2:0]; CMP/SUS; WPS. SRP, TB, SEC, QE, LB stay unchanged. */ - return !(sr1 & 0x1f) && !(sr2 & 0xc0) && !(sr3 & 0x04); + return !(sr1 & 0x03) && !(sr1 & 0x1c & ~bp_ok) && + !(sr2 & 0xc0) && !(sr3 & 0x04); } #endif diff --git a/drivers/spi/spi-apple-qspi.c b/drivers/spi/spi-apple-qspi.c index c594a241dcef8..e94fe6eb3fd5f 100644 --- a/drivers/spi/spi-apple-qspi.c +++ b/drivers/spi/spi-apple-qspi.c @@ -7,10 +7,10 @@ * command FIFO. This driver intentionally implements the PIO spi-mem path * only; that is sufficient for SPI NOR discovery and access. * - * On T8140 reads reach the whole boot NOR, but programs and erases are - * admitted only inside its unique, aligned nvram partition. Without that - * partition the NOR is read-only. - * + * The boot NOR starts read-only. On the qualified boards (J613, J713 and + * the J700) root may opt in at runtime to bounded, verified programs and + * 4 KiB erases inside the nvram partition; everything else stays + * read-only. On T8140 data reads reach the whole NOR. */ #include @@ -88,6 +88,10 @@ #define APPLE_QSPI_MAX_HZ 40000000 #define APPLE_QSPI_TIMEOUT_MS 1000 +/* Winbond W25Q64 (J613/J713) and W25Q16 (J700) boot NOR JEDEC IDs. */ +static const u8 apple_qspi_jedec_w25q64[3] = { 0xef, 0x65, 0x17 }; +static const u8 apple_qspi_jedec_w25q16[3] = { 0xef, 0x65, 0x15 }; + static DEFINE_MUTEX(apple_qspi_policy_lock); static bool read_only = true; static bool runtime_ready; @@ -125,7 +129,7 @@ static const struct kernel_param_ops apple_qspi_read_only_ops = { .get = apple_qspi_get_read_only, }; module_param_cb(read_only, &apple_qspi_read_only_ops, &read_only, 0644); -MODULE_PARM_DESC(read_only, "Default true; root runtime opt-in for bounded J613/J713 NVRAM writes"); +MODULE_PARM_DESC(read_only, "Default true; root runtime opt-in for bounded J613/J713/J700 NVRAM writes"); static bool apple_qspi_opcode_is_read(u8 opcode) { @@ -149,12 +153,16 @@ static bool apple_qspi_opcode_is_read(u8 opcode) } struct apple_qspi { - /* T8122/T8132 use the serialized, verified NVRAM policy. */ + /* Every write-path request goes through the verified NVRAM policy. */ bool nvram_policy; - /* T8140 admits programs and erases only inside nvram. */ + /* T8140: data reads of any shape pass straight to the controller. */ bool nvram_window; + /* The nvram partition [window_start, window_end). */ u64 window_start; u64 window_end; + /* Expected JEDEC ID and the SR1 block-protect bits left by firmware. */ + u8 jedec[3]; + u8 bp_ok; bool nvram_write_candidate; bool write_fault; void __iomem *base; @@ -652,7 +660,7 @@ static int apple_qspi_nvram_modify(struct spi_mem *mem, const struct spi_mem_op ret = apple_qspi_read_register(mem, 0x9f, id, sizeof(id)); if (ret) goto out; - if (id[0] != 0xef || id[1] != 0x65 || id[2] != 0x17) { + if (memcmp(id, qspi->jedec, sizeof(id))) { ret = -ENODEV; goto out; } @@ -660,7 +668,8 @@ static int apple_qspi_nvram_modify(struct spi_mem *mem, const struct spi_mem_op if (ret) goto out; /* No unlocking. Conservatively refuse BP/CMP/WPS, BUSY/WEL or suspend. */ - if (!apple_qspi_status_allows_write(before[0], before[1], before[2])) { + if (!apple_qspi_status_allows_write(before[0], before[1], before[2], + qspi->bp_ok)) { ret = -EACCES; goto out; } @@ -780,7 +789,8 @@ static int apple_qspi_exec_op(struct spi_mem *mem, const struct spi_mem_op *op) } if (!apple_qspi_mutation_allowed(op->cmd.opcode, op->addr.nbytes, op->addr.val, op->data.nbytes, - op->data.dir == SPI_MEM_DATA_OUT)) { + op->data.dir == SPI_MEM_DATA_OUT, + qspi->window_start, qspi->window_end)) { denial = "mutation-policy"; goto out; } @@ -802,36 +812,14 @@ static int apple_qspi_exec_op(struct spi_mem *mem, const struct spi_mem_op *op) return ret; } -/* T8140: data reads pass; anything else must stay inside the nvram window. */ -static int apple_qspi_window_exec_op(struct spi_mem *mem, - const struct spi_mem_op *op) -{ - struct apple_qspi *qspi = spi_controller_get_devdata(mem->spi->controller); - - if (op->data.dir == SPI_MEM_DATA_IN && op->data.nbytes) - return apple_qspi_transfer_op(mem, op); - if (!op->dummy.nbytes && - apple_qspi_window_allowed(op->cmd.opcode, op->addr.nbytes, - op->addr.val, op->data.nbytes, - op->data.dir == SPI_MEM_DATA_OUT, - qspi->window_start, qspi->window_end)) - return apple_qspi_transfer_op(mem, op); - - dev_warn_ratelimited(&mem->spi->dev, - "boot NOR request denied: opcode=%02x addr=%#llx addr_bytes=%u len=%u dir=%u nvram=[%#llx, %#llx)\n", - op->cmd.opcode, (unsigned long long)op->addr.val, - op->addr.nbytes, op->data.nbytes, op->data.dir, - (unsigned long long)qspi->window_start, - (unsigned long long)qspi->window_end); - return -EACCES; -} - static int apple_qspi_mem_exec_op(struct spi_mem *mem, const struct spi_mem_op *op) { struct apple_qspi *qspi = spi_controller_get_devdata(mem->spi->controller); - if (qspi->nvram_window) - return apple_qspi_window_exec_op(mem, op); + /* T8140 keeps its multi-line data reads; spi-mem serializes them. */ + if (qspi->nvram_window && op->data.dir == SPI_MEM_DATA_IN && + op->data.nbytes) + return apple_qspi_transfer_op(mem, op); if (!qspi->nvram_policy) return apple_qspi_transfer_op(mem, op); return apple_qspi_exec_op(mem, op); @@ -853,6 +841,9 @@ static bool apple_qspi_nvram_write_board(struct device_node *node, if (of_device_is_compatible(node, "apple,t8132-qspi")) return of_machine_is_compatible("apple,j713") && base == 0x3ad118000ULL; + if (of_device_is_compatible(node, "apple,t8140-qspi")) + return of_machine_is_compatible("apple,j700") && + base == 0x385110000ULL; return false; } @@ -894,7 +885,8 @@ static bool apple_qspi_nvram_layout(struct device_node *node) /* * The one "nvram" partition of the flash on CS0, 4 KiB aligned and within - * 32-bit addressing. Without it, no program or erase is admitted. + * three-byte addressing, since the policy only admits three-byte programs + * and erases. Without it, no program or erase is admitted. */ static void apple_qspi_nvram_window(struct device *dev, struct apple_qspi *qspi) { @@ -933,7 +925,7 @@ static void apple_qspi_nvram_window(struct device *dev, struct apple_qspi *qspi) } } of_node_put(parts); - if (found != 1 || start >= end || end > 1ULL << 32 || + if (found != 1 || start >= end || end > 1ULL << 24 || !IS_ALIGNED(start, APPLE_QSPI_ERASE_SIZE) || !IS_ALIGNED(end, APPLE_QSPI_ERASE_SIZE)) start = end = 0; @@ -941,8 +933,7 @@ static void apple_qspi_nvram_window(struct device *dev, struct apple_qspi *qspi) qspi->window_start = start; qspi->window_end = end; if (start < end) - dev_dbg(dev, "boot NOR read-only outside nvram [%#llx, %#llx)\n", - start, end); + dev_dbg(dev, "nvram partition [%#llx, %#llx)\n", start, end); else dev_dbg(dev, "boot NOR read-only: no usable nvram partition\n"); } @@ -961,11 +952,19 @@ static int apple_qspi_probe(struct platform_device *pdev) return -ENOMEM; qspi = spi_controller_get_devdata(ctlr); + qspi->nvram_policy = true; qspi->nvram_window = of_device_is_compatible(dev->of_node, "apple,t8140-qspi"); - if (qspi->nvram_window) + if (qspi->nvram_window) { + /* J700: W25Q16JV; iBoot leaves BP0 set over the top 64 KiB. */ apple_qspi_nvram_window(dev, qspi); - qspi->nvram_policy = of_device_is_compatible(dev->of_node, "apple,t8122-qspi") || - of_device_is_compatible(dev->of_node, "apple,t8132-qspi"); + memcpy(qspi->jedec, apple_qspi_jedec_w25q16, sizeof(qspi->jedec)); + qspi->bp_ok = 0x1c; + } else { + qspi->window_start = APPLE_QSPI_NVRAM_START; + qspi->window_end = APPLE_QSPI_NVRAM_END; + memcpy(qspi->jedec, apple_qspi_jedec_w25q64, sizeof(qspi->jedec)); + qspi->bp_ok = 0; + } if (of_device_is_compatible(dev->of_node, "apple,t8122-qspi") && !of_machine_is_compatible("apple,j613")) return -ENODEV; @@ -979,7 +978,8 @@ static int apple_qspi_probe(struct platform_device *pdev) qspi->nvram_write_candidate = apple_qspi_nvram_write_board(dev->of_node, res->start) && - apple_qspi_nvram_layout(dev->of_node); + (qspi->nvram_window ? qspi->window_start < qspi->window_end : + apple_qspi_nvram_layout(dev->of_node)); qspi->base = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(qspi->base)) return PTR_ERR(qspi->base); @@ -1016,7 +1016,7 @@ static int apple_qspi_probe(struct platform_device *pdev) ctlr->num_chipselect = 1; ctlr->max_native_cs = 1; ctlr->mode_bits = SPI_CPHA | SPI_CPOL; - if (!qspi->nvram_policy) + if (qspi->nvram_window) ctlr->mode_bits |= SPI_RX_DUAL | SPI_RX_QUAD | SPI_TX_DUAL | SPI_TX_QUAD; ctlr->bits_per_word_mask = SPI_BPW_MASK(8); diff --git a/tools/asahi/qspi-nvram-tests.py b/tools/asahi/qspi-nvram-tests.py index 5b48039e8dbab..427d0cd8479c0 100644 --- a/tools/asahi/qspi-nvram-tests.py +++ b/tools/asahi/qspi-nvram-tests.py @@ -68,6 +68,7 @@ def function(source, signature): struct apple_qspi { bool nvram_policy, nvram_write_candidate, write_fault, nvram_window; u64 window_start, window_end; + u8 jedec[3]; u8 bp_ok; }; struct spi_controller { struct apple_qspi *q; }; struct spi_device { struct spi_controller *controller; unsigned int cs; int dev; }; @@ -103,7 +104,7 @@ def function(source, signature): memcpy(o->data.buf.in,flash+o->addr.val,o->data.nbytes); if(corrupt_readback&&programmed)((u8*)o->data.buf.in)[0]^=1; break; - case 0x06:assert(!sr[0]);sr[0]|=2;break; + case 0x06:assert(!(sr[0]&3));sr[0]|=2;break; case 0x04:sr[0]&=~2;break; case 0x02: assert(sr[0]&2);assert(o->addr.val>=0x700000&&o->addr.val+o->data.nbytes<=0x800000); @@ -123,7 +124,7 @@ def function(source, signature): static struct spi_mem mem={&spi}; static struct kernel_param parameter={&read_only}; static void reset(void){ - memset(flash,0xff,sizeof(flash));memset(sr,0,sizeof(sr));q=(struct apple_qspi){.nvram_policy=true,.nvram_write_candidate=true}; + memset(flash,0xff,sizeof(flash));memset(sr,0,sizeof(sr));q=(struct apple_qspi){.nvram_policy=true,.nvram_write_candidate=true,.window_start=0x700000,.window_end=0x800000,.jedec={0xef,0x65,0x17}}; read_only=true;runtime_ready=false;write_fault_latched=false;wire_count=0;fail_opcode=0;spi.cs=0; corrupt_readback=false;stuck_busy=false;programmed=false;deny_alloc=false;jiffies=0; jedec[0]=0xef;jedec[1]=0x65;jedec[2]=0x17; @@ -136,7 +137,7 @@ def function(source, signature): static bool sent(unsigned int o){for(unsigned int i=0;i= APPLE_QSPI_NVRAM_START', 'addr >= 1'), + ('j700-board', 'of_machine_is_compatible("apple,j700")', 'true'), + ('range', 'addr >= start &&', 'addr >= 1 &&'), + ('bp-ok', '!(sr1 & 0x1c & ~bp_ok)', '1'), ('read-only', 'read_only || !qspi->nvram_write_candidate || qspi->write_fault', '!qspi->nvram_write_candidate || qspi->write_fault'), ('fault-latch', 'write_fault_latched = true;', 'write_fault_latched = false;')]: From 7907a2410557e3def4a9b729aa40d201ea8a5cb8 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Fri, 30 Oct 2026 05:59:37 +0000 Subject: [PATCH 2/6] arm64: configs: asahi: Use 4 KiB SPI NOR erase sectors The J700 nvram partition starts at 0x24000 on its boot NOR. SPI-NOR picks the largest uniform erase size unless MTD_SPI_NOR_USE_4K_SECTORS is set, so the flash reports 64 KiB erase blocks and MTD forces the partition read-only because it does not start on an erase boundary. asahi-bless then cannot set a boot volume. There is no device tree or per-flash way to choose the erase size; this option is the only switch, and it is the upstream default. The Fedora base config turns it off. Set it again. The Apple NVRAM partitions are managed in 4 KiB units, and the QSPI NVRAM policy on the J613, J713 and J700 only admits 4 KiB erases anyway. Other SPI NOR users only see smaller erase blocks. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/configs/asahi.config | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arch/arm64/configs/asahi.config b/arch/arm64/configs/asahi.config index 3c8c4d225985e..7e106922b1c9a 100644 --- a/arch/arm64/configs/asahi.config +++ b/arch/arm64/configs/asahi.config @@ -51,6 +51,8 @@ CONFIG_APPLE_WATCHDOG=y # m1n1 hands the ADT and its own log over as phram regions (/dev/mtd*). CONFIG_MTD=y CONFIG_MTD_PHRAM=m +# Apple NVRAM partitions are 4 KiB aligned (J700: 0x24000). +CONFIG_MTD_SPI_NOR_USE_4K_SECTORS=y CONFIG_VIDEO_APPLE_AVD=m CONFIG_VIDEO_APPLE_AVE=m CONFIG_VIDEO_APPLE_ISP=m From 427136e56f4461a38186a6c6bdc44a3d336030c1 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sat, 31 Oct 2026 01:53:55 +0000 Subject: [PATCH 3/6] spi: apple-qspi: describe the qualified NVRAM boards as data Each board that may write its NVRAM was qualified by its own compatible check in apple_qspi_nvram_write_board(), and probe then picked the partition bounds, JEDEC ID and block-protect allowance from whether the controller was T8140. The J613/J713 partition was a pair of constants, checked by a second device tree walk, while the J700 one came from the device tree unchecked. Put the qualified boards in one table: controller compatible, machine, controller aperture, the nvram partition the device tree must describe, the flash JEDEC ID and the block-protect bits its firmware leaves set. Every controller now reads its nvram partition from the device tree with the same walk, and a board is a write candidate only when that partition is exactly the one in its table entry. The T8140 multi-line read pass-through comes from the compatible match data. The J613 and J713 keep the 0x700000 partition and the W25Q64 ID, and the J700 keeps 0x24000, the W25Q16 ID and its BP tolerance, now with its partition pinned as well. The KUnit 0x700000 case covers the partition edges against its neighbours, and the host harness reads the table and gains controller, aperture and partition controls. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/spi/spi-apple-qspi-policy-test.c | 42 +++++-- drivers/spi/spi-apple-qspi-policy.h | 3 - drivers/spi/spi-apple-qspi.c | 144 ++++++++++++----------- tools/asahi/qspi-nvram-tests.py | 23 +++- 4 files changed, 125 insertions(+), 87 deletions(-) diff --git a/drivers/spi/spi-apple-qspi-policy-test.c b/drivers/spi/spi-apple-qspi-policy-test.c index b1ecbdbe4e26e..34563b7b37ac4 100644 --- a/drivers/spi/spi-apple-qspi-policy-test.c +++ b/drivers/spi/spi-apple-qspi-policy-test.c @@ -105,14 +105,42 @@ static void qspi_policy_empty(struct kunit *test) NV_START, NV_START)); } -static void qspi_policy_j613(struct kunit *test) +/* The J613 and J713 nvram partition: 0x700000, 1 MiB. */ +#define NV7_START 0x700000ULL +#define NV7_END 0x800000ULL + +static int nv7(unsigned int opcode, unsigned int addr_bytes, + unsigned long long addr, unsigned int len, int data_out) { - unsigned long long s = APPLE_QSPI_NVRAM_START, e = APPLE_QSPI_NVRAM_END; + return apple_qspi_mutation_allowed(opcode, addr_bytes, addr, len, + data_out, NV7_START, NV7_END); +} - KUNIT_EXPECT_TRUE(test, apple_qspi_mutation_allowed(0x02, 3, s, 256, 1, s, e)); - KUNIT_EXPECT_TRUE(test, apple_qspi_mutation_allowed(0x20, 3, e - 0x1000, 0, 0, s, e)); - KUNIT_EXPECT_FALSE(test, apple_qspi_mutation_allowed(0x02, 3, s - 1, 1, 1, s, e)); - KUNIT_EXPECT_FALSE(test, apple_qspi_mutation_allowed(0x20, 3, e, 0, 0, s, e)); +static void qspi_policy_nvram_700000(struct kunit *test) +{ + /* First and last page and sector of the partition. */ + KUNIT_EXPECT_TRUE(test, nv7(0x02, 3, NV7_START, 256, 1)); + KUNIT_EXPECT_TRUE(test, nv7(0x02, 3, NV7_END - 256, 256, 1)); + KUNIT_EXPECT_TRUE(test, nv7(0x20, 3, NV7_START, 0, 0)); + KUNIT_EXPECT_TRUE(test, nv7(0x20, 3, NV7_END - 0x1000, 0, 0)); + /* syscfg ends at 0x700000 and the NOR is 8 MiB: both stay out. */ + KUNIT_EXPECT_FALSE(test, nv7(0x02, 3, NV7_START - 1, 1, 1)); + KUNIT_EXPECT_FALSE(test, nv7(0x02, 3, NV7_START - 256, 256, 1)); + KUNIT_EXPECT_FALSE(test, nv7(0x20, 3, NV7_START - 0x1000, 0, 0)); + KUNIT_EXPECT_FALSE(test, nv7(0x02, 3, NV7_END, 1, 1)); + KUNIT_EXPECT_FALSE(test, nv7(0x02, 3, NV7_END - 1, 2, 1)); + KUNIT_EXPECT_FALSE(test, nv7(0x20, 3, NV7_END, 0, 0)); + /* iBoot at 0 and the J700 window are outside this partition. */ + KUNIT_EXPECT_FALSE(test, nv7(0x02, 3, 0, 256, 1)); + KUNIT_EXPECT_FALSE(test, nv7(0x20, 3, NV_START, 0, 0)); + /* Larger erases and four-byte forms are still refused here. */ + KUNIT_EXPECT_FALSE(test, nv7(0x52, 3, NV7_START, 0, 0)); + KUNIT_EXPECT_FALSE(test, nv7(0xd8, 3, NV7_START, 0, 0)); + KUNIT_EXPECT_FALSE(test, nv7(0x21, 4, NV7_START, 0, 0)); + KUNIT_EXPECT_FALSE(test, nv7(0x12, 4, NV7_START, 256, 1)); + /* An address above 16 MiB is not aliased onto the partition. */ + KUNIT_EXPECT_FALSE(test, nv7(0x02, 3, (1ULL << 24) + NV7_START, 1, 1)); + KUNIT_EXPECT_FALSE(test, nv7(0x20, 3, (1ULL << 24) + NV7_START, 0, 0)); } static void qspi_policy_status(struct kunit *test) @@ -136,7 +164,7 @@ static struct kunit_case apple_qspi_policy_cases[] = { KUNIT_CASE(qspi_policy_refused_commands), KUNIT_CASE(qspi_policy_address_width), KUNIT_CASE(qspi_policy_empty), - KUNIT_CASE(qspi_policy_j613), + KUNIT_CASE(qspi_policy_nvram_700000), KUNIT_CASE(qspi_policy_status), {} }; diff --git a/drivers/spi/spi-apple-qspi-policy.h b/drivers/spi/spi-apple-qspi-policy.h index 12d8ac41dd26c..43c14008a6a32 100644 --- a/drivers/spi/spi-apple-qspi-policy.h +++ b/drivers/spi/spi-apple-qspi-policy.h @@ -2,9 +2,6 @@ #ifndef APPLE_QSPI_POLICY_H #define APPLE_QSPI_POLICY_H -/* The pinned J613/J713 nvram partition. */ -#define APPLE_QSPI_NVRAM_START 0x700000ULL -#define APPLE_QSPI_NVRAM_END 0x800000ULL #define APPLE_QSPI_PAGE_SIZE 256U #define APPLE_QSPI_ERASE_SIZE 4096U diff --git a/drivers/spi/spi-apple-qspi.c b/drivers/spi/spi-apple-qspi.c index e94fe6eb3fd5f..bce570395547d 100644 --- a/drivers/spi/spi-apple-qspi.c +++ b/drivers/spi/spi-apple-qspi.c @@ -88,9 +88,40 @@ #define APPLE_QSPI_MAX_HZ 40000000 #define APPLE_QSPI_TIMEOUT_MS 1000 -/* Winbond W25Q64 (J613/J713) and W25Q16 (J700) boot NOR JEDEC IDs. */ -static const u8 apple_qspi_jedec_w25q64[3] = { 0xef, 0x65, 0x17 }; -static const u8 apple_qspi_jedec_w25q16[3] = { 0xef, 0x65, 0x15 }; +/* Per-SoC controller differences, from the compatible. */ +struct apple_qspi_soc { + /* T8140: data reads of any shape pass straight to the controller. */ + bool multi_io_reads; +}; + +/* + * A board whose NVRAM may be written: the controller compatible, the + * machine, the controller aperture, the nvram partition the device tree + * must describe, the boot NOR JEDEC ID and the SR1 block-protect bits its + * firmware leaves set at boot. + */ +struct apple_qspi_board { + const char *controller; + const char *machine; + resource_size_t base; + u32 nvram_start; + u32 nvram_size; + u8 jedec[3]; + u8 bp_ok; +}; + +#define APPLE_QSPI_JEDEC_W25Q64 { 0xef, 0x65, 0x17 } +#define APPLE_QSPI_JEDEC_W25Q16 { 0xef, 0x65, 0x15 } + +static const struct apple_qspi_board apple_qspi_boards[] = { + { "apple,t8122-qspi", "apple,j613", 0x2a1118000ULL, + 0x700000, 0x100000, APPLE_QSPI_JEDEC_W25Q64, 0 }, + { "apple,t8132-qspi", "apple,j713", 0x3ad118000ULL, + 0x700000, 0x100000, APPLE_QSPI_JEDEC_W25Q64, 0 }, + /* iBoot leaves BP0 set on the J700, over the top 64 KiB. */ + { "apple,t8140-qspi", "apple,j700", 0x385110000ULL, + 0x24000, 0x1d0000, APPLE_QSPI_JEDEC_W25Q16, 0x1c }, +}; static DEFINE_MUTEX(apple_qspi_policy_lock); static bool read_only = true; @@ -129,7 +160,7 @@ static const struct kernel_param_ops apple_qspi_read_only_ops = { .get = apple_qspi_get_read_only, }; module_param_cb(read_only, &apple_qspi_read_only_ops, &read_only, 0644); -MODULE_PARM_DESC(read_only, "Default true; root runtime opt-in for bounded J613/J713/J700 NVRAM writes"); +MODULE_PARM_DESC(read_only, "Default true; root runtime opt-in for bounded NVRAM writes on qualified boards"); static bool apple_qspi_opcode_is_read(u8 opcode) { @@ -604,7 +635,7 @@ static int apple_qspi_transfer_op(struct spi_mem *mem, return ret; } -/* Private transfers used by the serialized J613 write policy. */ +/* Private transfers used by the serialized NVRAM write policy. */ static int apple_qspi_read_register(struct spi_mem *mem, u8 opcode, u8 *buf, u32 len) { struct spi_mem_op op = SPI_MEM_OP(SPI_MEM_OP_CMD(opcode, 1), @@ -832,55 +863,20 @@ static const struct spi_controller_mem_ops apple_qspi_mem_ops = { }; /* Keep write qualification tied to the board and its controller aperture. */ -static bool apple_qspi_nvram_write_board(struct device_node *node, - resource_size_t base) +static const struct apple_qspi_board * +apple_qspi_nvram_write_board(struct device_node *node, resource_size_t base) { - if (of_device_is_compatible(node, "apple,t8122-qspi")) - return of_machine_is_compatible("apple,j613") && - base == 0x2a1118000ULL; - if (of_device_is_compatible(node, "apple,t8132-qspi")) - return of_machine_is_compatible("apple,j713") && - base == 0x3ad118000ULL; - if (of_device_is_compatible(node, "apple,t8140-qspi")) - return of_machine_is_compatible("apple,j700") && - base == 0x385110000ULL; - return false; -} - -/* Require the pinned partition and single physical chip select. */ -static bool apple_qspi_nvram_layout(struct device_node *node) -{ - struct device_node *flash, *parts, *part; - u32 reg[2], cs; - const char *label; - bool found = false; - - /* OF name matching excludes the unit address (@0); reg pins CS0. */ - flash = of_get_child_by_name(node, "flash"); - if (!flash) - return false; - if (!of_device_is_compatible(flash, "jedec,spi-nor") || - of_property_read_u32(flash, "reg", &cs) || cs) { - of_node_put(flash); - return false; - } - parts = of_get_child_by_name(flash, "partitions"); - of_node_put(flash); - if (!parts) - return false; - if (of_device_is_compatible(parts, "fixed-partitions")) { - for_each_available_child_of_node(parts, part) { - if (!of_property_read_string(part, "label", &label) && - !strcmp(label, "nvram") && - of_property_count_u32_elems(part, "reg") == 2 && - !of_property_read_u32_array(part, "reg", reg, 2) && - reg[0] == APPLE_QSPI_NVRAM_START && - reg[1] == APPLE_QSPI_NVRAM_END - APPLE_QSPI_NVRAM_START) - found = true; - } + const struct apple_qspi_board *board; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(apple_qspi_boards); i++) { + board = &apple_qspi_boards[i]; + if (of_device_is_compatible(node, board->controller) && + of_machine_is_compatible(board->machine) && + base == board->base) + return board; } - of_node_put(parts); - return found; + return NULL; } /* @@ -941,30 +937,26 @@ static void apple_qspi_nvram_window(struct device *dev, struct apple_qspi *qspi) static int apple_qspi_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; + const struct apple_qspi_board *board; + const struct apple_qspi_soc *soc; struct spi_controller *ctlr; struct apple_qspi *qspi; struct resource *res; u32 depths, version; int irq, ret; + soc = of_device_get_match_data(dev); + if (!soc) + return -ENODEV; + ctlr = devm_spi_alloc_host(dev, sizeof(*qspi)); if (!ctlr) return -ENOMEM; qspi = spi_controller_get_devdata(ctlr); qspi->nvram_policy = true; - qspi->nvram_window = of_device_is_compatible(dev->of_node, "apple,t8140-qspi"); - if (qspi->nvram_window) { - /* J700: W25Q16JV; iBoot leaves BP0 set over the top 64 KiB. */ - apple_qspi_nvram_window(dev, qspi); - memcpy(qspi->jedec, apple_qspi_jedec_w25q16, sizeof(qspi->jedec)); - qspi->bp_ok = 0x1c; - } else { - qspi->window_start = APPLE_QSPI_NVRAM_START; - qspi->window_end = APPLE_QSPI_NVRAM_END; - memcpy(qspi->jedec, apple_qspi_jedec_w25q64, sizeof(qspi->jedec)); - qspi->bp_ok = 0; - } + qspi->nvram_window = soc->multi_io_reads; + apple_qspi_nvram_window(dev, qspi); if (of_device_is_compatible(dev->of_node, "apple,t8122-qspi") && !of_machine_is_compatible("apple,j613")) return -ENODEV; @@ -976,10 +968,14 @@ static int apple_qspi_probe(struct platform_device *pdev) return dev_err_probe(dev, -EINVAL, "QSPIMC aperture is too small\n"); - qspi->nvram_write_candidate = apple_qspi_nvram_write_board(dev->of_node, - res->start) && - (qspi->nvram_window ? qspi->window_start < qspi->window_end : - apple_qspi_nvram_layout(dev->of_node)); + /* The device tree must describe exactly the board's nvram partition. */ + board = apple_qspi_nvram_write_board(dev->of_node, res->start); + if (board && qspi->window_start == board->nvram_start && + qspi->window_end == (u64)board->nvram_start + board->nvram_size) { + memcpy(qspi->jedec, board->jedec, sizeof(qspi->jedec)); + qspi->bp_ok = board->bp_ok; + qspi->nvram_write_candidate = true; + } qspi->base = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(qspi->base)) return PTR_ERR(qspi->base); @@ -1125,10 +1121,16 @@ static int apple_qspi_resume(struct device *dev) static DEFINE_SIMPLE_DEV_PM_OPS(apple_qspi_pm_ops, apple_qspi_suspend, apple_qspi_resume); +static const struct apple_qspi_soc apple_qspi_single_io = { }; + +static const struct apple_qspi_soc apple_qspi_t8140 = { + .multi_io_reads = true, +}; + static const struct of_device_id apple_qspi_of_match[] = { - { .compatible = "apple,t8122-qspi" }, - { .compatible = "apple,t8132-qspi" }, - { .compatible = "apple,t8140-qspi" }, + { .compatible = "apple,t8122-qspi", .data = &apple_qspi_single_io }, + { .compatible = "apple,t8132-qspi", .data = &apple_qspi_single_io }, + { .compatible = "apple,t8140-qspi", .data = &apple_qspi_t8140 }, {} }; MODULE_DEVICE_TABLE(of, apple_qspi_of_match); diff --git a/tools/asahi/qspi-nvram-tests.py b/tools/asahi/qspi-nvram-tests.py index 427d0cd8479c0..930e41a1354bb 100644 --- a/tools/asahi/qspi-nvram-tests.py +++ b/tools/asahi/qspi-nvram-tests.py @@ -5,6 +5,7 @@ import argparse import contextlib import json +import re import subprocess import tempfile @@ -145,9 +146,15 @@ def function(source, signature): for(unsigned int c=0;cnvram_start==0x700000&&qb->nvram_size==0x100000&&!memcmp(qb->jedec,jedec,3)&&!qb->bp_ok); + machine="apple,j700";qb=apple_qspi_nvram_write_board(&t8140,0x385110000ULL); + assert(qb&&qb->nvram_start==0x24000&&qb->nvram_size==0x1d0000&&qb->jedec[2]==0x15&&qb->bp_ok==0x1c); puts("PASS J613/J713/J700 board, controller and full-width aperture qualification (60 combinations)"); u8 data[256];memset(data,0x55,sizeof(data));reset(); struct spi_mem_op wren=SPI_MEM_OP(SPI_MEM_OP_CMD(0x06,1),SPI_MEM_OP_NO_ADDR,SPI_MEM_OP_NO_DUMMY,SPI_MEM_OP_NO_DATA); @@ -245,15 +252,18 @@ def main(): args.output.mkdir(parents=True, exist_ok=True) source = (ROOT / 'drivers/spi/spi-apple-qspi.c').read_text() policy = (ROOT / 'drivers/spi/spi-apple-qspi-policy.h').read_text() + boards = (''.join(m.group(0) + '\n' for m in re.finditer(r'#define APPLE_QSPI_JEDEC_\w+.*', source)) + + function(source, 'struct apple_qspi_board {').rstrip() + ';\n' + + function(source, 'static const struct apple_qspi_board apple_qspi_boards[]').rstrip() + ';\n') signatures = ['static int apple_qspi_set_read_only(', 'static int apple_qspi_get_read_only(', - 'static bool apple_qspi_nvram_write_board(', + 'static const struct apple_qspi_board *\napple_qspi_nvram_write_board(', 'static bool apple_qspi_opcode_is_read(', 'static int apple_qspi_width_mode(', 'static int apple_qspi_shape_op(', 'static bool apple_qspi_supports_op(', 'static int apple_qspi_read_register(', 'static int apple_qspi_command(', 'static int apple_qspi_read_array(', 'static int apple_qspi_status(', 'static int apple_qspi_nvram_modify(', 'static int apple_qspi_exec_op(', 'static int apple_qspi_mem_exec_op('] - code = PREFIX + policy + '\n'.join(function(source, s) for s in signatures) + CONTROLS + code = PREFIX + policy + boards + '\n'.join(function(source, s) for s in signatures) + CONTROLS output = contextlib.nullcontext(str(args.output)) if args.output else tempfile.TemporaryDirectory(prefix='qspi-nvram-') with output as directory: current = run(code, Path(directory) / 'current.c') @@ -261,9 +271,10 @@ def main(): if current.returncode: raise SystemExit(current.stderr) for name, old, new in [ - ('j713-board', 'of_machine_is_compatible("apple,j713")', 'true'), - ('j713-aperture', 'base == 0x3ad118000ULL', 'base != 0'), - ('j700-board', 'of_machine_is_compatible("apple,j700")', 'true'), + ('controller', 'of_device_is_compatible(node, board->controller) &&', ''), + ('board', 'of_machine_is_compatible(board->machine) &&', ''), + ('aperture', 'base == board->base', 'base != 0'), + ('j713-partition', '"apple,j713", 0x3ad118000ULL,\n\t 0x700000', '"apple,j713", 0x3ad118000ULL,\n\t 0x680000'), ('range', 'addr >= start &&', 'addr >= 1 &&'), ('bp-ok', '!(sr1 & 0x1c & ~bp_ok)', '1'), ('read-only', 'read_only || !qspi->nvram_write_candidate || qspi->write_fault', From 5c9287e18940bcc86a7696aa3f756d6c2d53b0e1 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sat, 31 Oct 2026 01:54:06 +0000 Subject: [PATCH 4/6] spi: apple-qspi: add T6041 and admit NVRAM writes on the J616s The M4 Pro (T6041) boot NOR sits behind the same QSPIMC controller as the M3, M4 and A18 Pro parts, at 0x429118000. The J616s ADT places its nvram partition (anvram, "nvram,nor") at 0x700000 with a size of 1 MiB, the J613 and J713 layout, next to iboot at 0 and syscfg at 0x680000. Without a T6041 match the driver does not bind, so asahi-bless cannot select a boot volume from Linux. Match apple,t6041-qspi with single-line transfers, as on T8122 and T8132. Qualify the J616s for the verified NVRAM policy at its controller aperture and that partition, with the W25Q64 ID the other 0x700000 boards use and no block-protect bits tolerated. The flash ID is checked before every program or erase, so a different part is refused before any write enable reaches it. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/spi/Kconfig | 7 ++++--- drivers/spi/spi-apple-qspi-policy-test.c | 4 ++-- drivers/spi/spi-apple-qspi.c | 11 +++++++---- tools/asahi/qspi-nvram-tests.py | 17 +++++++++-------- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig index 8b53e2cf36b8d..dbaf0d6949249 100644 --- a/drivers/spi/Kconfig +++ b/drivers/spi/Kconfig @@ -146,10 +146,11 @@ config SPI_APPLE_QSPI help This enables support for Apple's command-FIFO QSPIMC controller. Unlike the ordinary Apple SPIMC block, QSPIMC accepts packed SPI - memory descriptors and is used for the boot NOR on T8140 systems. + memory descriptors and is used for the boot NOR on M3 and later + systems. - The boot NOR defaults to read-only access. On the J613, J713 and - J700 a runtime opt-in admits bounded, verified page programs and + The boot NOR defaults to read-only access. On the J613, J713, J616s + and J700 a runtime opt-in admits bounded, verified page programs and 4 KiB erases inside the nvram partition only; the rest of the boot NOR stays read-only. diff --git a/drivers/spi/spi-apple-qspi-policy-test.c b/drivers/spi/spi-apple-qspi-policy-test.c index 34563b7b37ac4..8271f7a19f06d 100644 --- a/drivers/spi/spi-apple-qspi-policy-test.c +++ b/drivers/spi/spi-apple-qspi-policy-test.c @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0 /* * KUnit tests for the Apple QSPIMC NVRAM mutation policy, at the J700 and - * J613/J713 nvram partitions. + * J613/J713/J616s nvram partitions. */ #include @@ -105,7 +105,7 @@ static void qspi_policy_empty(struct kunit *test) NV_START, NV_START)); } -/* The J613 and J713 nvram partition: 0x700000, 1 MiB. */ +/* The J613, J713 and J616s nvram partition: 0x700000, 1 MiB. */ #define NV7_START 0x700000ULL #define NV7_END 0x800000ULL diff --git a/drivers/spi/spi-apple-qspi.c b/drivers/spi/spi-apple-qspi.c index bce570395547d..1c4d6794803ca 100644 --- a/drivers/spi/spi-apple-qspi.c +++ b/drivers/spi/spi-apple-qspi.c @@ -7,10 +7,10 @@ * command FIFO. This driver intentionally implements the PIO spi-mem path * only; that is sufficient for SPI NOR discovery and access. * - * The boot NOR starts read-only. On the qualified boards (J613, J713 and - * the J700) root may opt in at runtime to bounded, verified programs and - * 4 KiB erases inside the nvram partition; everything else stays - * read-only. On T8140 data reads reach the whole NOR. + * The boot NOR starts read-only. On the qualified boards (J613, J713, + * J616s and the J700) root may opt in at runtime to bounded, verified + * programs and 4 KiB erases inside the nvram partition; everything else + * stays read-only. On T8140 data reads reach the whole NOR. */ #include @@ -118,6 +118,8 @@ static const struct apple_qspi_board apple_qspi_boards[] = { 0x700000, 0x100000, APPLE_QSPI_JEDEC_W25Q64, 0 }, { "apple,t8132-qspi", "apple,j713", 0x3ad118000ULL, 0x700000, 0x100000, APPLE_QSPI_JEDEC_W25Q64, 0 }, + { "apple,t6041-qspi", "apple,j616s", 0x429118000ULL, + 0x700000, 0x100000, APPLE_QSPI_JEDEC_W25Q64, 0 }, /* iBoot leaves BP0 set on the J700, over the top 64 KiB. */ { "apple,t8140-qspi", "apple,j700", 0x385110000ULL, 0x24000, 0x1d0000, APPLE_QSPI_JEDEC_W25Q16, 0x1c }, @@ -1130,6 +1132,7 @@ static const struct apple_qspi_soc apple_qspi_t8140 = { static const struct of_device_id apple_qspi_of_match[] = { { .compatible = "apple,t8122-qspi", .data = &apple_qspi_single_io }, { .compatible = "apple,t8132-qspi", .data = &apple_qspi_single_io }, + { .compatible = "apple,t6041-qspi", .data = &apple_qspi_single_io }, { .compatible = "apple,t8140-qspi", .data = &apple_qspi_t8140 }, {} }; diff --git a/tools/asahi/qspi-nvram-tests.py b/tools/asahi/qspi-nvram-tests.py index 930e41a1354bb..3939be8208c5b 100644 --- a/tools/asahi/qspi-nvram-tests.py +++ b/tools/asahi/qspi-nvram-tests.py @@ -138,24 +138,24 @@ def function(source, signature): static bool sent(unsigned int o){for(unsigned int i=0;invram_start==0x700000&&qb->nvram_size==0x100000&&!memcmp(qb->jedec,jedec,3)&&!qb->bp_ok); machine="apple,j700";qb=apple_qspi_nvram_write_board(&t8140,0x385110000ULL); assert(qb&&qb->nvram_start==0x24000&&qb->nvram_size==0x1d0000&&qb->jedec[2]==0x15&&qb->bp_ok==0x1c); - puts("PASS J613/J713/J700 board, controller and full-width aperture qualification (60 combinations)"); + puts("PASS J613/J713/J700/J616s board, controller and full-width aperture qualification (140 combinations)"); u8 data[256];memset(data,0x55,sizeof(data));reset(); struct spi_mem_op wren=SPI_MEM_OP(SPI_MEM_OP_CMD(0x06,1),SPI_MEM_OP_NO_ADDR,SPI_MEM_OP_NO_DUMMY,SPI_MEM_OP_NO_DATA); /* T8140 (J700) shares the verified policy at its DT nvram window; its data @@ -274,7 +274,8 @@ def main(): ('controller', 'of_device_is_compatible(node, board->controller) &&', ''), ('board', 'of_machine_is_compatible(board->machine) &&', ''), ('aperture', 'base == board->base', 'base != 0'), - ('j713-partition', '"apple,j713", 0x3ad118000ULL,\n\t 0x700000', '"apple,j713", 0x3ad118000ULL,\n\t 0x680000'), + ('j616s-aperture', '"apple,j616s", 0x429118000ULL', '"apple,j616s", 0x429118800ULL'), + ('j616s-partition', '"apple,j616s", 0x429118000ULL,\n\t 0x700000', '"apple,j616s", 0x429118000ULL,\n\t 0x680000'), ('range', 'addr >= start &&', 'addr >= 1 &&'), ('bp-ok', '!(sr1 & 0x1c & ~bp_ok)', '1'), ('read-only', 'read_only || !qspi->nvram_write_candidate || qspi->write_fault', From 3d9601c360bfb63b89bee2226b2d5d3fd9f86d26 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sat, 31 Oct 2026 01:54:14 +0000 Subject: [PATCH 5/6] arm64: dts: apple: t6040-j616s: enable the boot NOR and its nvram Describe the J616s boot NOR on the QSPIMC controller with the nvram partition from its ADT (/arm-io/qspi/spinor/anvram, 0x700000, 1 MiB), so asahi-nvram and asahi-bless can find it. Use single-line transfers at 10 MHz, as on the J713. Add the nvram and spi0 aliases the J700 also carries. The SoC node stays disabled for boards that do not describe their flash. Assisted-by: LLM Signed-off-by: Ryan Murray --- arch/arm64/boot/dts/apple/t6040-j616s.dts | 29 +++++++++++++++++++++++ arch/arm64/boot/dts/apple/t6040.dtsi | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index 69f0070af7659..d9ee12939c232 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -21,6 +21,8 @@ aliases { keyboard = &keyboard; + nvram = &nvram; + spi0 = &qspi; wifi0 = "/pcie@1cb0000000/pci@0,0/wifi@0,0"; bluetooth0 = "/pcie@1cb0000000/pci@0,0/bluetooth@0,1"; /* used by the loader to hand over the display */ @@ -1430,6 +1432,33 @@ status = "okay"; }; +/* + * Boot NOR. The partition is the J616s ADT /arm-io/qspi/spinor/anvram, + * flash-relative; asahi-bless and asahi-nvram find it by its label. + */ +&qspi { + status = "okay"; + + flash@0 { + compatible = "jedec,spi-nor"; + reg = <0>; + spi-max-frequency = <10000000>; + spi-rx-bus-width = <1>; + spi-tx-bus-width = <1>; + + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + nvram: partition@700000 { + label = "nvram"; + reg = <0x700000 0x100000>; + }; + }; + }; +}; + /* * Touch ID sensor (Mesa 0x3356). Its power is NUB GPIO 53 and its * data-ready line is AP GPIO 116, as the J616s ADT describes them. diff --git a/arch/arm64/boot/dts/apple/t6040.dtsi b/arch/arm64/boot/dts/apple/t6040.dtsi index 6c54e05caa743..f2c603b3f01e6 100644 --- a/arch/arm64/boot/dts/apple/t6040.dtsi +++ b/arch/arm64/boot/dts/apple/t6040.dtsi @@ -2124,7 +2124,7 @@ power-domains = <&ps_qspi>; #address-cells = <1>; #size-cells = <0>; - /* The firmware NOR needs a qualified partition/write policy. */ + /* Boards enable it with their flash and its nvram partition. */ status = "disabled"; }; From eac79241035dcc1a1268fb7e96e833e1adea8fa4 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sat, 31 Oct 2026 02:09:04 +0000 Subject: [PATCH 6/6] spi: apple-qspi: use the J616s Macronix boot NOR ID and registers macOS reports the J616s boot NOR JEDEC ID as c22b27, a Macronix part, not the Winbond W25Q64 the J613 and J713 carry. Pin that ID in the J616s table entry. The write policy and the flash_status attribute read status registers with Winbond opcodes: 05h, 35h and 15h. On Macronix parts 35h is EQIO, which switches the flash to QPI mode, and the status register keeps four block-protect bits, BP[3:0] at bits 5:2, where Winbond has three and TB. For a Macronix part, read only the status (05h) and configuration (15h) registers, refuse 35h from SPI-NOR as well, and check WIP, WEL and all four BP bits before a write. flash_status reads the JEDEC ID first and sends 35h only to Winbond parts. The J616s still admits only 3-byte page programs and 4 KiB sector erases (20h) inside its nvram partition, with readback verification. SPI-NOR has no table entry for c22b27; it binds through the generic Macronix entry and takes geometry and erase types from SFDP. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/spi/spi-apple-qspi-policy-test.c | 19 +++++ drivers/spi/spi-apple-qspi-policy.h | 14 ++++ drivers/spi/spi-apple-qspi.c | 89 ++++++++++++++++++------ tools/asahi/qspi-nvram-tests.py | 20 +++++- 4 files changed, 119 insertions(+), 23 deletions(-) diff --git a/drivers/spi/spi-apple-qspi-policy-test.c b/drivers/spi/spi-apple-qspi-policy-test.c index 8271f7a19f06d..f6dc555057443 100644 --- a/drivers/spi/spi-apple-qspi-policy-test.c +++ b/drivers/spi/spi-apple-qspi-policy-test.c @@ -157,6 +157,24 @@ static void qspi_policy_status(struct kunit *test) KUNIT_EXPECT_FALSE(test, apple_qspi_status_allows_write(0, 0, 0x04, 0x1c)); } +static void qspi_policy_mx_status(struct kunit *test) +{ + /* The J616s Macronix part: no block protection is tolerated. */ + KUNIT_EXPECT_TRUE(test, apple_qspi_mx_status_allows_write(0x00, 0)); + /* QE and SRWD stay as found and do not block a write. */ + KUNIT_EXPECT_TRUE(test, apple_qspi_mx_status_allows_write(0x40, 0)); + KUNIT_EXPECT_TRUE(test, apple_qspi_mx_status_allows_write(0x80, 0)); + /* WIP and WEL are never admitted. */ + KUNIT_EXPECT_FALSE(test, apple_qspi_mx_status_allows_write(0x01, 0x3c)); + KUNIT_EXPECT_FALSE(test, apple_qspi_mx_status_allows_write(0x02, 0x3c)); + /* Each of BP0 to BP3, including BP3 at bit 5, which Winbond calls TB. */ + KUNIT_EXPECT_FALSE(test, apple_qspi_mx_status_allows_write(0x04, 0)); + KUNIT_EXPECT_FALSE(test, apple_qspi_mx_status_allows_write(0x08, 0)); + KUNIT_EXPECT_FALSE(test, apple_qspi_mx_status_allows_write(0x10, 0)); + KUNIT_EXPECT_FALSE(test, apple_qspi_mx_status_allows_write(0x20, 0)); + KUNIT_EXPECT_TRUE(test, apple_qspi_mx_status_allows_write(0x3c, 0x3c)); +} + static struct kunit_case apple_qspi_policy_cases[] = { KUNIT_CASE(qspi_policy_write_enable), KUNIT_CASE(qspi_policy_program), @@ -166,6 +184,7 @@ static struct kunit_case apple_qspi_policy_cases[] = { KUNIT_CASE(qspi_policy_empty), KUNIT_CASE(qspi_policy_nvram_700000), KUNIT_CASE(qspi_policy_status), + KUNIT_CASE(qspi_policy_mx_status), {} }; diff --git a/drivers/spi/spi-apple-qspi-policy.h b/drivers/spi/spi-apple-qspi-policy.h index 43c14008a6a32..5acd9a62d5291 100644 --- a/drivers/spi/spi-apple-qspi-policy.h +++ b/drivers/spi/spi-apple-qspi-policy.h @@ -62,4 +62,18 @@ static inline int apple_qspi_status_allows_write(unsigned int sr1, return !(sr1 & 0x03) && !(sr1 & 0x1c & ~bp_ok) && !(sr2 & 0xc0) && !(sr3 & 0x04); } + +/* JEDEC manufacturer ID of Macronix parts. */ +#define APPLE_QSPI_MFR_MACRONIX 0xc2 + +/* + * The Macronix status register holds WIP, WEL and four block-protect bits, + * BP[3:0] at bits 5:2. BUSY and WEL are always refused, and block-protect + * bits only where bp_ok allows them. QE and SRWD stay unchanged. + */ +static inline int apple_qspi_mx_status_allows_write(unsigned int sr, + unsigned int bp_ok) +{ + return !(sr & 0x03) && !(sr & 0x3c & ~bp_ok); +} #endif diff --git a/drivers/spi/spi-apple-qspi.c b/drivers/spi/spi-apple-qspi.c index 1c4d6794803ca..f4b4b6a0d5202 100644 --- a/drivers/spi/spi-apple-qspi.c +++ b/drivers/spi/spi-apple-qspi.c @@ -112,14 +112,16 @@ struct apple_qspi_board { #define APPLE_QSPI_JEDEC_W25Q64 { 0xef, 0x65, 0x17 } #define APPLE_QSPI_JEDEC_W25Q16 { 0xef, 0x65, 0x15 } +#define APPLE_QSPI_JEDEC_MX_2B27 { 0xc2, 0x2b, 0x27 } static const struct apple_qspi_board apple_qspi_boards[] = { { "apple,t8122-qspi", "apple,j613", 0x2a1118000ULL, 0x700000, 0x100000, APPLE_QSPI_JEDEC_W25Q64, 0 }, { "apple,t8132-qspi", "apple,j713", 0x3ad118000ULL, 0x700000, 0x100000, APPLE_QSPI_JEDEC_W25Q64, 0 }, + /* Macronix; the ID macOS reports for the J616s boot NOR. */ { "apple,t6041-qspi", "apple,j616s", 0x429118000ULL, - 0x700000, 0x100000, APPLE_QSPI_JEDEC_W25Q64, 0 }, + 0x700000, 0x100000, APPLE_QSPI_JEDEC_MX_2B27, 0 }, /* iBoot leaves BP0 set on the J700, over the top 64 KiB. */ { "apple,t8140-qspi", "apple,j700", 0x385110000ULL, 0x24000, 0x1d0000, APPLE_QSPI_JEDEC_W25Q16, 0x1c }, @@ -664,12 +666,24 @@ static int apple_qspi_read_array(struct spi_mem *mem, u64 addr, u8 *buf, u32 len return apple_qspi_transfer_op(mem, &op); } -static int apple_qspi_status(struct spi_mem *mem, u8 sr[3]) +/* + * Winbond parts have three status registers (05h, 35h, 15h). On Macronix + * parts 35h is EQIO, which switches the flash to QPI mode, so only the + * status (05h) and configuration (15h) registers are read there. + */ +static int apple_qspi_status(struct spi_mem *mem, u8 mfr, u8 sr[3]) { - static const u8 opcode[] = { 0x05, 0x35, 0x15 }; - int i, ret; - - for (i = 0; i < 3; i++) { + static const u8 winbond[] = { 0x05, 0x35, 0x15 }; + static const u8 macronix[] = { 0x05, 0x15 }; + const u8 *opcode = winbond; + int i, n = ARRAY_SIZE(winbond), ret; + + if (mfr == APPLE_QSPI_MFR_MACRONIX) { + opcode = macronix; + n = ARRAY_SIZE(macronix); + } + sr[2] = 0; + for (i = 0; i < n; i++) { ret = apple_qspi_read_register(mem, opcode[i], &sr[i], 1); if (ret) return ret; @@ -697,11 +711,13 @@ static int apple_qspi_nvram_modify(struct spi_mem *mem, const struct spi_mem_op ret = -ENODEV; goto out; } - ret = apple_qspi_status(mem, before); + ret = apple_qspi_status(mem, id[0], before); if (ret) goto out; /* No unlocking. Conservatively refuse BP/CMP/WPS, BUSY/WEL or suspend. */ - if (!apple_qspi_status_allows_write(before[0], before[1], before[2], + if (id[0] == APPLE_QSPI_MFR_MACRONIX ? + !apple_qspi_mx_status_allows_write(before[0], qspi->bp_ok) : + !apple_qspi_status_allows_write(before[0], before[1], before[2], qspi->bp_ok)) { ret = -EACCES; goto out; @@ -755,7 +771,7 @@ static int apple_qspi_nvram_modify(struct spi_mem *mem, const struct spi_mem_op ret = apple_qspi_command(mem, 0x04); if (ret) goto fault; - ret = apple_qspi_status(mem, after); + ret = apple_qspi_status(mem, id[0], after); if (ret) goto fault; if (memcmp(before, after, sizeof(before))) { @@ -804,6 +820,12 @@ static int apple_qspi_exec_op(struct spi_mem *mem, const struct spi_mem_op *op) if (!apple_qspi_supports_op(mem, op) || apple_qspi_shape_op(op, &shape)) return -EOPNOTSUPP; mutex_lock(&apple_qspi_policy_lock); + /* 35h is EQIO, not a register read, on Macronix parts. */ + if (op->cmd.opcode == 0x35 && + qspi->jedec[0] == APPLE_QSPI_MFR_MACRONIX) { + ret = -EOPNOTSUPP; + goto out; + } if (op->data.dir == SPI_MEM_DATA_IN && op->data.nbytes && apple_qspi_opcode_is_read(op->cmd.opcode)) { ret = apple_qspi_transfer_op(mem, op); @@ -1043,15 +1065,24 @@ static int apple_qspi_match_cs0(struct device *dev, const void *unused) !spi_get_chipselect(to_spi_device(dev), 0); } +static int apple_qspi_flash_read_reg(struct spi_mem *mem, u8 opcode, + u8 *buf, unsigned int len) +{ + struct spi_mem_op op = SPI_MEM_OP(SPI_MEM_OP_CMD(opcode, 1), + SPI_MEM_OP_NO_ADDR, SPI_MEM_OP_NO_DUMMY, + SPI_MEM_OP_DATA_IN(len, buf, 1)); + + return spi_mem_exec_op(mem, &op); +} + static ssize_t flash_status_show(struct device *dev, struct device_attribute *attr, char *buf) { - static const u8 opcodes[] = { 0x9f, 0x05, 0x35, 0x15 }; struct spi_controller *ctlr = dev_get_drvdata(dev); struct spi_mem mem = { }; struct device *child; u8 *data; - int i, ret; + int ret; child = device_find_child(&ctlr->dev, NULL, apple_qspi_match_cs0); if (!child) @@ -1065,18 +1096,34 @@ static ssize_t flash_status_show(struct device *dev, /* spi-mem supplies queue, bus and I/O serialization for each read. * These are sequential samples, not an atomic protection snapshot. + * The JEDEC ID decides which registers exist: 35h is SR2 on Winbond + * parts but EQIO on Macronix ones, so it is sent to Winbond only. */ - for (i = 0; i < ARRAY_SIZE(opcodes); i++) { - struct spi_mem_op op = SPI_MEM_OP(SPI_MEM_OP_CMD(opcodes[i], 1), - SPI_MEM_OP_NO_ADDR, SPI_MEM_OP_NO_DUMMY, - SPI_MEM_OP_DATA_IN(i ? 1 : 3, data + (i ? i + 2 : 0), 1)); - - ret = spi_mem_exec_op(&mem, &op); - if (ret) - goto out_free; + ret = apple_qspi_flash_read_reg(&mem, 0x9f, data, 3) ?: + apple_qspi_flash_read_reg(&mem, 0x05, data + 3, 1); + if (ret) + goto out_free; + switch (data[0]) { + case 0xef: + ret = apple_qspi_flash_read_reg(&mem, 0x35, data + 4, 1) ?: + apple_qspi_flash_read_reg(&mem, 0x15, data + 5, 1); + if (!ret) + ret = sysfs_emit(buf, "jedec=%02x%02x%02x sr1=%02x sr2=%02x sr3=%02x\n", + data[0], data[1], data[2], data[3], + data[4], data[5]); + break; + case APPLE_QSPI_MFR_MACRONIX: + ret = apple_qspi_flash_read_reg(&mem, 0x15, data + 4, 1); + if (!ret) + ret = sysfs_emit(buf, "jedec=%02x%02x%02x sr=%02x cr=%02x\n", + data[0], data[1], data[2], data[3], + data[4]); + break; + default: + ret = sysfs_emit(buf, "jedec=%02x%02x%02x sr1=%02x\n", + data[0], data[1], data[2], data[3]); + break; } - ret = sysfs_emit(buf, "jedec=%02x%02x%02x sr1=%02x sr2=%02x sr3=%02x\n", - data[0], data[1], data[2], data[3], data[4], data[5]); out_free: kfree(data); out_put: diff --git a/tools/asahi/qspi-nvram-tests.py b/tools/asahi/qspi-nvram-tests.py index 3939be8208c5b..b458f3b06f3a8 100644 --- a/tools/asahi/qspi-nvram-tests.py +++ b/tools/asahi/qspi-nvram-tests.py @@ -98,7 +98,7 @@ def function(source, signature): switch(o->cmd.opcode){ case 0x9f:memcpy(o->data.buf.in,jedec,o->data.nbytes);break; case 0x05:*(u8*)o->data.buf.in=sr[0]|(stuck_busy&&programmed?1:0);break; - case 0x35:*(u8*)o->data.buf.in=sr[1];break; + case 0x35:assert(jedec[0]!=0xc2);*(u8*)o->data.buf.in=sr[1];break; case 0x15:*(u8*)o->data.buf.in=sr[2];break; case 0x03: assert(o->addr.val+o->data.nbytes<=sizeof(flash)); @@ -152,7 +152,7 @@ def function(source, signature): const struct apple_qspi_board *qb; struct device_node t6041={"apple,t6041-qspi"},t8140={"apple,t8140-qspi"}; machine="apple,j616s";qb=apple_qspi_nvram_write_board(&t6041,0x429118000ULL); - assert(qb&&qb->nvram_start==0x700000&&qb->nvram_size==0x100000&&!memcmp(qb->jedec,jedec,3)&&!qb->bp_ok); + assert(qb&&qb->nvram_start==0x700000&&qb->nvram_size==0x100000&&qb->jedec[0]==0xc2&&qb->jedec[1]==0x2b&&qb->jedec[2]==0x27&&!qb->bp_ok); machine="apple,j700";qb=apple_qspi_nvram_write_board(&t8140,0x385110000ULL); assert(qb&&qb->nvram_start==0x24000&&qb->nvram_size==0x1d0000&&qb->jedec[2]==0x15&&qb->bp_ok==0x1c); puts("PASS J613/J713/J700/J616s board, controller and full-width aperture qualification (140 combinations)"); @@ -223,6 +223,19 @@ def function(source, signature): assert(sent(0x04)&&apple_qspi_set_read_only("N",¶meter)==-EPERM);denied(o); } puts("PASS read-only startup, exact opcode/range/page/erase admission, WREN ordering, readback and fault latch"); + /* J616s Macronix part: 35h (EQIO there) never reaches the wire. */ + reset();jedec[0]=0xc2;jedec[1]=0x2b;jedec[2]=0x27;memcpy(q.jedec,jedec,3);arm();memset(data,0x55,sizeof(data)); + o=program(0x700000,256,data);wire_count=0;assert(!apple_qspi_exec_op(&mem,&o)&&sent(0x02)&&!sent(0x35)&&sent(0x15)); + assert(!memcmp(flash+0x700000,data,256)&&!sr[0]); + erase.addr.val=0x701000; + wire_count=0;assert(!apple_qspi_exec_op(&mem,&erase)&&sent(0x20)&&!sent(0x35)); + struct spi_mem_op rdcr2=SPI_MEM_OP(SPI_MEM_OP_CMD(0x35,1),SPI_MEM_OP_NO_ADDR,SPI_MEM_OP_NO_DUMMY,SPI_MEM_OP_DATA_IN(1,id,1)); + wire_count=0;assert(apple_qspi_exec_op(&mem,&rdcr2)<0&&!wire_count); + for(unsigned int bp=0x04;bp<=0x20;bp<<=1){reset();jedec[0]=0xc2;jedec[1]=0x2b;jedec[2]=0x27;memcpy(q.jedec,jedec,3);arm(); + sr[0]=bp;wire_count=0;o=program(0x700000,1,data);assert(apple_qspi_exec_op(&mem,&o)==-EACCES&&!sent(0x06)&&!sent(0x35));} + reset();memcpy(q.jedec,(u8[]){0xc2,0x2b,0x27},3);arm();wire_count=0;o=program(0x700000,1,data); + assert(apple_qspi_exec_op(&mem,&o)==-ENODEV&&!sent(0x06)); + puts("PASS Macronix J616s part: no 35h on the wire, SR/CR checks, BP0-BP3 refusal and JEDEC mismatch"); return 0; } ''' @@ -278,6 +291,9 @@ def main(): ('j616s-partition', '"apple,j616s", 0x429118000ULL,\n\t 0x700000', '"apple,j616s", 0x429118000ULL,\n\t 0x680000'), ('range', 'addr >= start &&', 'addr >= 1 &&'), ('bp-ok', '!(sr1 & 0x1c & ~bp_ok)', '1'), + ('mx-bp3', '!(sr & 0x3c & ~bp_ok)', '!(sr & 0x1c & ~bp_ok)'), + ('mx-eqio', 'opcode = macronix;', 'opcode = winbond;'), + ('mx-eqio-read', 'qspi->jedec[0] == APPLE_QSPI_MFR_MACRONIX) {', '0) {'), ('read-only', 'read_only || !qspi->nvram_write_candidate || qspi->write_fault', '!qspi->nvram_write_candidate || qspi->write_fault'), ('fault-latch', 'write_fault_latched = true;', 'write_fault_latched = false;')]: