From 7e7bdc88d1565af6f15c4629c7bb3a03a7d79239 Mon Sep 17 00:00:00 2001 From: Ryan Leung Date: Sat, 23 May 2026 15:31:42 +1000 Subject: [PATCH] uboot-mediatek: mtd: nand: spi: otp: add NULL guards to OTP size functions U-Boot v2026.01 introduced drivers/mtd/nand/spi/otp.c which adds `spinand_user_otp_size()` and `spinand_fact_otp_size()`. These functions are called unconditionally from `spinand_init()` in drivers/mtd/nand/spi/core.c to determine whether to set up OTP methods: if (spinand_user_otp_size(spinand) || spinand_fact_otp_size(spinand)) { ret = spinand_set_mtd_otp_ops(spinand); /* drivers/mtd/nand/spi/core.c:2369 */ ... } Both OTP size functions pass `&spinand->user_otp->layout` or `&spinand->fact_otp->layout` to `spinand_otp_size()` without first checking whether the pointer is NULL. In the standard probing path, these pointers are assigned by `spinand_match_and_init()` from the per-chip `spinand_info` table. `100-21-mtd-spi-nand-add-CASN-page-support.patch` adds CASN detection to `spinand_detect()` so that when a CASN page is found, `spinand_init_via_casn()` is called instead of `spinand_id_detect()`. As `spinand_match_and_init()` is only called via `spinand_id_detect()`, it is never invoked for CASN-probed devices. As a result, `spinand->user_otp` and `spinand->fact_otp` remain NULL from the zero-initialised DM priv allocation. Both `spinand_user_otp_size()` and `spinand_fact_otp_size()` spuriously return non-zero by reading `layout->npages` from a small mapped address computed from their respective NULL pointer, causing `spinand_init()` to call `spinand_set_mtd_otp_ops()` which reads `spinand->user_otp->ops` and thereby silently assigns a garbage value to `user_ops` that is then dereferenced at `if (user_ops->info)`. int spinand_set_mtd_otp_ops(struct spinand_device *spinand) { struct mtd_info *mtd = spinand_to_mtd(spinand); const struct spinand_fact_otp_ops *fact_ops = spinand->fact_otp->ops; const struct spinand_user_otp_ops *user_ops = spinand->user_otp->ops; ... if (user_ops) { if (user_ops->info) /* drivers/mtd/nand/spi/otp.c:343 */ ... ... } ... } On COMFAST CF-WR632AX (MT7981, Winbond W25N01GV) this results in a "Synchronous Abort" during boot. The crash was confirmed via `addr2line` on the U-Boot ELF: "Synchronous Abort" handler, esr 0x96000004, far 0xeafffffeeafffffe elr: 0000000041e2ff44 (drivers/mtd/nand/spi/otp.c:343) lr : 0000000041e2f2f8 (drivers/mtd/nand/spi/core.c:2369) The same was observed on Acer Predator Connect W6x (MT7986, Winbond W25N02KV): "Synchronous Abort" handler, esr 0x96000004, far 0xeafffffeeafffffe elr: 0000000041e2c868 (drivers/mtd/nand/spi/otp.c:343) lr : 0000000041e2bc1c (drivers/mtd/nand/spi/core.c:2369) Fix this by adding NULL guards to both OTP size functions so that they return 0 when the pointer is NULL. With both functions returning 0, the condition in `spinand_init()` evaluates to false and `spinand_set_mtd_otp_ops()` is never called, skipping setup of OTP methods entirely for CASN-probed SPI NAND chips. This change has no effect on SPI NAND chips that do have OTP data but not CASN. For those chips, `spinand_match_and_init()` sets `spinand->user_otp` and `spinand->fact_otp` to non-NULL values from the per-chip table, the NULL guard does not activate, and the existing behaviour is unchanged. Fixes: https://github.com/openwrt/openwrt/issues/23430 Fixes: b94de14bafd06660536691ed633f364edf5fbe4d ("uboot-mediatek: update to v2026.01") Signed-off-by: Ryan Leung Link: https://github.com/openwrt/openwrt/pull/23447 Signed-off-by: Hauke Mehrtens --- ...dd-NULL-guards-to-OTP-size-functions.patch | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 package/boot/uboot-mediatek/patches/111-mtd-nand-spi-otp-add-NULL-guards-to-OTP-size-functions.patch diff --git a/package/boot/uboot-mediatek/patches/111-mtd-nand-spi-otp-add-NULL-guards-to-OTP-size-functions.patch b/package/boot/uboot-mediatek/patches/111-mtd-nand-spi-otp-add-NULL-guards-to-OTP-size-functions.patch new file mode 100644 index 0000000000..40f81f888d --- /dev/null +++ b/package/boot/uboot-mediatek/patches/111-mtd-nand-spi-otp-add-NULL-guards-to-OTP-size-functions.patch @@ -0,0 +1,91 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ryan Leung +Date: Wed, 20 May 2026 15:16:13 +1000 +Subject: [PATCH] mtd: nand: spi: otp: add NULL guards to OTP size functions + +U-Boot v2026.01 introduced drivers/mtd/nand/spi/otp.c which adds `spinand_user_otp_size()` and +`spinand_fact_otp_size()`. These functions are called unconditionally from `spinand_init()` in +drivers/mtd/nand/spi/core.c to determine whether to set up OTP methods: + + if (spinand_user_otp_size(spinand) || spinand_fact_otp_size(spinand)) { + ret = spinand_set_mtd_otp_ops(spinand); /* drivers/mtd/nand/spi/core.c:2369 */ + ... + } + +Both OTP size functions pass `&spinand->user_otp->layout` or `&spinand->fact_otp->layout` to +`spinand_otp_size()` without first checking whether the pointer is NULL. In the standard probing +path, these pointers are assigned by `spinand_match_and_init()` from the per-chip `spinand_info` +table. `100-21-mtd-spi-nand-add-CASN-page-support.patch` adds CASN detection to `spinand_detect()` +so that when a CASN page is found, `spinand_init_via_casn()` is called instead of +`spinand_id_detect()`. As `spinand_match_and_init()` is only called via `spinand_id_detect()`, it +is never invoked for CASN-probed devices. + +As a result, `spinand->user_otp` and `spinand->fact_otp` remain NULL from the zero-initialised DM +priv allocation. Both `spinand_user_otp_size()` and `spinand_fact_otp_size()` spuriously return +non-zero by reading `layout->npages` from a small mapped address computed from their respective +NULL pointer, causing `spinand_init()` to call `spinand_set_mtd_otp_ops()` which reads +`spinand->user_otp->ops` and thereby silently assigns a garbage value to `user_ops` that is then +dereferenced at `if (user_ops->info)`. + + int spinand_set_mtd_otp_ops(struct spinand_device *spinand) + { + struct mtd_info *mtd = spinand_to_mtd(spinand); + const struct spinand_fact_otp_ops *fact_ops = spinand->fact_otp->ops; + const struct spinand_user_otp_ops *user_ops = spinand->user_otp->ops; + ... + if (user_ops) { + if (user_ops->info) /* drivers/mtd/nand/spi/otp.c:343 */ + ... + ... + } + ... + } + +On COMFAST CF-WR632AX (MT7981, Winbond W25N01GV) this results in a "Synchronous Abort" during boot. +The crash was confirmed via `addr2line` on the U-Boot ELF: + + "Synchronous Abort" handler, esr 0x96000004, far 0xeafffffeeafffffe + elr: 0000000041e2ff44 (drivers/mtd/nand/spi/otp.c:343) + lr : 0000000041e2f2f8 (drivers/mtd/nand/spi/core.c:2369) + +The same was observed on Acer Predator Connect W6x (MT7986, Winbond W25N02KV): + + "Synchronous Abort" handler, esr 0x96000004, far 0xeafffffeeafffffe + elr: 0000000041e2c868 (drivers/mtd/nand/spi/otp.c:343) + lr : 0000000041e2bc1c (drivers/mtd/nand/spi/core.c:2369) + +Fix this by adding NULL guards to both OTP size functions so that they return 0 when the pointer is +NULL. With both functions returning 0, the condition in `spinand_init()` evaluates to false and +`spinand_set_mtd_otp_ops()` is never called, skipping setup of OTP methods entirely for CASN-probed +SPI NAND chips. + +This change has no effect on SPI NAND chips that do have OTP data but not CASN. For those chips, +`spinand_match_and_init()` sets `spinand->user_otp` and `spinand->fact_otp` to non-NULL values from +the per-chip table, the NULL guard does not activate, and the existing behaviour is unchanged. + +Fixes: b94de14bafd06660536691ed633f364edf5fbe4d ("uboot-mediatek: update to v2026.01") +Signed-off-by: Ryan Leung +--- + drivers/mtd/nand/spi/otp.c | 4 ++++ + 1 file changed, 4 insertions(+) + +--- a/drivers/mtd/nand/spi/otp.c ++++ b/drivers/mtd/nand/spi/otp.c +@@ -39,6 +39,8 @@ static size_t spinand_otp_size(struct sp + */ + size_t spinand_fact_otp_size(struct spinand_device *spinand) + { ++ if (!spinand->fact_otp) ++ return 0; + return spinand_otp_size(spinand, &spinand->fact_otp->layout); + } + +@@ -50,6 +52,8 @@ size_t spinand_fact_otp_size(struct spin + */ + size_t spinand_user_otp_size(struct spinand_device *spinand) + { ++ if (!spinand->user_otp) ++ return 0; + return spinand_otp_size(spinand, &spinand->user_otp->layout); + } +