From e6b96b2d4b0d6f174ca884d579b649adfd074466 Mon Sep 17 00:00:00 2001 From: Shayne Legault <8741557+shaynelegault@users.noreply.github.com> Date: Fri, 6 Nov 2026 12:53:52 -0600 Subject: [PATCH 1/4] p105: record bounded native NAND reader evidence Document native VFL/SwissPPNFTL recovery, saved-capture crosschecks, KUnit results and the latest finite provider lifetime. Keep both logical storage capabilities missing until exact-revision device qualification. Record the host and ARM64 test limits separately from hardware evidence. All 34 catalog tests pass. Matrix regeneration leaves capability rows unchanged; the P105 device and A5 chip schemas validate. Assisted-by: LLM --- docs/bring-up/p105-sftl-reader.md | 76 +++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 docs/bring-up/p105-sftl-reader.md diff --git a/docs/bring-up/p105-sftl-reader.md b/docs/bring-up/p105-sftl-reader.md new file mode 100644 index 00000000..cf8bddc1 --- /dev/null +++ b/docs/bring-up/p105-sftl-reader.md @@ -0,0 +1,76 @@ +# P105 native VFL and SwissPPNFTL reader + +H2FMI exposes physical PPN pages. Apple's VFL and SwissPPNFTL records supply +the logical medium above them. The proposed `apple-sftl` kernel driver reads +those records and publishes a read-only `applesftlN` disk. LwVM partition +mapping belongs above that disk, where encrypted iOS contents retain their +encryption. + +The measured profile has two buses and eight CAUs per bus, each with 2,088 +blocks of 128 normal-view pages. H2FMI supplies each 16 KiB data page with its +64 metadata bytes through a canonical provider. Context reads select the +separate SLC address view. This provider offers no program or erase callback. + +Recovery requires agreeing VFL context replicas and lane tables. It validates +the saved SFTL context fragments, then replays later data and trim records in +sequence order. Completed-block summaries preserve allocation floors; open +blocks are scanned fully so an erased lane cannot hide a later completed +stripe. Malformed or incomplete maps prevent disk registration. Unsupported +geometry returns an error. + +The map contains 15,625,016 entries, including sixteen internal keys. The +exported medium contains 15,625,000 logical 4 KiB pages, or 64,000,000,000 bytes, +and accepts 512-byte sector reads. The frontend serializes access to its +preallocated request buffer and drains requests before releasing the provider. + +## Bounded qualification + +The latest reader candidate is +[`e7d118292d828b9796d85d19872653f7d4a8cb67`](https://github.com/shaynelegault/linux/commit/e7d118292d828b9796d85d19872653f7d4a8cb67). +It follows the canonical H2FMI provider and native-map reader commits +`9b494217444fc61adae2f95bc2ec05e461d45049` and +`e5960c537455b9c40c57deb53823e80ca9b13bb4`. + +`max_page_reads` defaults to 32,768 physical page requests across recovery and +subsequent block reads. `max_io_time_ms` defaults to 600,000 milliseconds from +probe. Both read-only module parameters must be positive. The driver checks +the deadline before and after each provider transaction. One already-bounded +transaction limits the overrun. Exhausting either policy or receiving a +provider error permanently stops that reader instance, including later reads +that require no physical I/O. These limits define a qualification session; +a mounted root cannot depend on the reader beyond its lifetime. + +## Software checks and limits + +The unchanged mapping core reconstructed a saved private capture through +sequence 187831212. An independent Python reference agreed at all 28,830 +checked endpoints of the map union. Both LwVM headers and the System prefix +matched previously captured native iOS logical reads. This comparison used +saved pages and performed zero device operations. Captured pages remain +private; public fixtures generate synthetic media. + +At `e5960c537`, eight tests passed in an ARM64 QEMU KUnit kernel built from +pristine pinned Linux v6.12 plus the exact reader/test sources and required +Kbuild entries. This ran the mapping code without Cascadia or an H2FMI +hardware backend. The host version of those fixtures also passed under +ASan/UBSan with allocation, mutex and tree-API shims. + +The canonical H2FMI provider shares controller code with the imported +two-CAU profile. That profile needs a regression check; native-map recovery +currently accepts only the measured 64 GB geometry. + +The lifetime revision adds five boundary tests for page budgets, deadlines, +provider errors and invalid policies. Its actual core/test fixture passed +again under ASan/UBSan. The changed frontend and test objects compiled against +the current ARMv7 generated-header build with `W=1` and warnings-as-errors, +retaining the existing `vmstat.h` enum-arithmetic warning as a warning. The +earlier core, frontend, fixture and provider object targets also compiled. +These checks establish source behavior and compilation; the new lifetime +tests have no separate kernel KUnit run recorded here. + +The exact candidate still needs a complete qualification image and an +approved P105 boot with working USB recovery, followed by bounded native-map +reads. Host reconstruction and an ARM64 KUnit kernel do not qualify ARMv7 +controller timing. P105 `storage_read` and `storage_write` remain `missing` +while this native logical provider awaits device qualification. Sustained +writes and interruption recovery belong to the dependent writer work. From 4fccf193c9a1d91c80943b12cfbda1ab58846f2c Mon Sep 17 00:00:00 2001 From: Henrik Klee Date: Sat, 7 Nov 2026 20:30:59 +0800 Subject: [PATCH 2/4] kernel: apple-armv7 iOS disk reads Add the PPN page provider and the Apple VFL/SwissPPNFTL translation layer of #89 to kernel/patches/armv7/mtd, built into the apple-armv7 kernel. The 64 GB iPad mini 1's iOS disk appears as a read-only applesftlN, and its LwVM partitions with it. The read budget and lifetime module parameters are gone, so a mounted root reads for as long as the system runs; a provider error still stops the reader. Each patch keeps Shayne Legault as author and notes the changes and his original commit IDs. The bring-up notes and the board README follow the driver as carried. Assisted-by: LLM --- devices/p105/README.md | 11 +- docs/bring-up/p105-sftl-reader.md | 23 +- kernel/configs/apple-armv7/config | 1 + ...Provide-page-reads-and-programs-to-t.patch | 193 ++ ...e-VFL-and-SwissPPNFTL-translation-la.patch | 1840 +++++++++++++++++ 5 files changed, 2045 insertions(+), 23 deletions(-) create mode 100644 kernel/patches/armv7/mtd/0003-mtd-apple-h2fmi-Provide-page-reads-and-programs-to-t.patch create mode 100644 kernel/patches/armv7/mtd/0004-mtd-Add-the-Apple-VFL-and-SwissPPNFTL-translation-la.patch diff --git a/devices/p105/README.md b/devices/p105/README.md index dfac9ea9..407c2177 100644 --- a/devices/p105/README.md +++ b/devices/p105/README.md @@ -29,10 +29,11 @@ public recipe or release. ## Open work -- Storage: the NAND driver (OrchardLinux/orchard#86, #87) is in - `kernel/patches/armv7/mtd`. The iOS disk translation layer and its append - writes (#89, #91) follow on top of it. Reclamation (#93) is unfinished: - until it lands, writes stop once the blank flash blocks are used up. +- Storage: the NAND driver (OrchardLinux/orchard#86, #87) and the iOS disk + translation layer (#89) are in `kernel/patches/armv7/mtd`; the disk is + `applesftlN`, on the 64 GB model only, read-only. Its append writes (#91) + follow on top. Reclamation (#93) is unfinished: until it lands, writes + stop once the blank flash blocks are used up. - A Linux root on LwVM: the kernel reads LwVM partitions (`kernel/patches/armv7/block`), but the shared initrd finds its root by reading a GPT itself and needs to learn LwVM partitions. @@ -48,3 +49,5 @@ public recipe or release. is part of the driver. - #90's gadget config change (39b60b1, built-in CDC gadget off): not taken yet; it waits until 32-bit boots bind their own configfs gadget. +- #89's read budget and lifetime module parameters (ffa74e8, source + e7d118292) and its module build (a607325): dropped; the reader is built in. diff --git a/docs/bring-up/p105-sftl-reader.md b/docs/bring-up/p105-sftl-reader.md index cf8bddc1..7a8b2cad 100644 --- a/docs/bring-up/p105-sftl-reader.md +++ b/docs/bring-up/p105-sftl-reader.md @@ -31,14 +31,9 @@ It follows the canonical H2FMI provider and native-map reader commits `9b494217444fc61adae2f95bc2ec05e461d45049` and `e5960c537455b9c40c57deb53823e80ca9b13bb4`. -`max_page_reads` defaults to 32,768 physical page requests across recovery and -subsequent block reads. `max_io_time_ms` defaults to 600,000 milliseconds from -probe. Both read-only module parameters must be positive. The driver checks -the deadline before and after each provider transaction. One already-bounded -transaction limits the overrun. Exhausting either policy or receiving a -provider error permanently stops that reader instance, including later reads -that require no physical I/O. These limits define a qualification session; -a mounted root cannot depend on the reader beyond its lifetime. +Orchard carries this reader in `kernel/patches/armv7/mtd` without the read +budget and lifetime module parameters of that candidate: a mounted root reads +for as long as the system runs. A provider error still stops the reader. ## Software checks and limits @@ -59,18 +54,8 @@ The canonical H2FMI provider shares controller code with the imported two-CAU profile. That profile needs a regression check; native-map recovery currently accepts only the measured 64 GB geometry. -The lifetime revision adds five boundary tests for page budgets, deadlines, -provider errors and invalid policies. Its actual core/test fixture passed -again under ASan/UBSan. The changed frontend and test objects compiled against -the current ARMv7 generated-header build with `W=1` and warnings-as-errors, -retaining the existing `vmstat.h` enum-arithmetic warning as a warning. The -earlier core, frontend, fixture and provider object targets also compiled. -These checks establish source behavior and compilation; the new lifetime -tests have no separate kernel KUnit run recorded here. - The exact candidate still needs a complete qualification image and an approved P105 boot with working USB recovery, followed by bounded native-map reads. Host reconstruction and an ARM64 KUnit kernel do not qualify ARMv7 -controller timing. P105 `storage_read` and `storage_write` remain `missing` -while this native logical provider awaits device qualification. Sustained +controller timing. P105 `storage_read` stays `untested` until then. Sustained writes and interruption recovery belong to the dependent writer work. diff --git a/kernel/configs/apple-armv7/config b/kernel/configs/apple-armv7/config index 4ea49f95..733f9098 100644 --- a/kernel/configs/apple-armv7/config +++ b/kernel/configs/apple-armv7/config @@ -1451,6 +1451,7 @@ CONFIG_MTD_OF_PARTS=y # CONFIG_MTD_OOPS is not set # CONFIG_MTD_SWAP is not set # CONFIG_MTD_PARTITIONED_MASTER is not set +CONFIG_MTD_APPLE_SFTL=y # # RAM/ROM/Flash chip drivers diff --git a/kernel/patches/armv7/mtd/0003-mtd-apple-h2fmi-Provide-page-reads-and-programs-to-t.patch b/kernel/patches/armv7/mtd/0003-mtd-apple-h2fmi-Provide-page-reads-and-programs-to-t.patch new file mode 100644 index 00000000..3c5f9849 --- /dev/null +++ b/kernel/patches/armv7/mtd/0003-mtd-apple-h2fmi-Provide-page-reads-and-programs-to-t.patch @@ -0,0 +1,193 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Shayne Legault <8741557+shaynelegault@users.noreply.github.com> +Date: Fri, 6 Nov 2026 11:48:07 -0600 +Subject: [PATCH] mtd: apple-h2fmi: Provide page reads and programs to the FTL + +Give the flash translation layer one provider for the whole NAND: it reads +complete pages, in the normal or the SLC context address view, and programs +them through the verified program path. Every call holds the bus lock for +the whole controller transaction and returns data and OOB separately. A +retired page returns its status without copying stale bytes. + +Register the apple-sftl child for the supported 64 GB package geometry, +and remove it before the controller goes away. + +[Orchard: from shaynelegault/orchard#89 ffa74e8 (source e7d118292) and + #91 e049bb4 (source ab137f3be); folded the program callback into this + patch; shortened the commit message] + +Assisted-by: LLM +--- + drivers/mtd/devices/mtd-apple-h2fmi.c | 90 +++++++++++++++++++++++++++ + include/linux/mtd/apple-ppn.h | 33 ++++++++++ + 2 files changed, 123 insertions(+) + create mode 100644 include/linux/mtd/apple-ppn.h + +diff --git a/drivers/mtd/devices/mtd-apple-h2fmi.c b/drivers/mtd/devices/mtd-apple-h2fmi.c +index a8f89c004d52..46a6bc603454 100644 +--- a/drivers/mtd/devices/mtd-apple-h2fmi.c ++++ b/drivers/mtd/devices/mtd-apple-h2fmi.c +@@ -34,6 +34,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -107,6 +108,7 @@ struct h2fmi { + struct mtd_info *mtd; + struct h2fmi_die *die; + unsigned int ndies; ++ struct platform_device *sftl; + }; + + /* +@@ -596,6 +598,85 @@ static int h2fmi_read_page_at(struct mtd_info *mtd, loff_t from, + return ret; + } + ++/* Canonical normal/context reads share the same complete transaction lock. */ ++static int h2fmi_ppn_read(void *priv, unsigned int bus, unsigned int cau, ++ unsigned int block, unsigned int page, bool context, ++ u8 *data, u8 *oob, u8 *status) ++{ ++ struct h2fmi *fmi = priv; ++ struct h2fmi_bus *b; ++ u32 row; ++ int ret; ++ ++ if (bus >= H2FMI_BUSES || !data || !oob || !status) ++ return -EINVAL; ++ b = &fmi->bus[bus]; ++ if (context) ++ ret = h2fmi_slc_row(&b->geometry, cau, block, page, &row); ++ else ++ ret = h2fmi_page_row(&b->geometry, cau, block, page, &row); ++ if (ret) ++ return ret; ++ ++ /* Retired pages have no data phase. Never return a stale page buffer. */ ++ memset(data, 0, H2FMI_PAGE_DATA); ++ memset(oob, 0, H2FMI_PAGE_OOB); ++ mutex_lock(&b->lock); ++ ret = h2fmi_read_transaction(fmi, b, 0x0a, 0x37, row, 4, ++ b->page, H2FMI_PAGE_RAW, status); ++ if (!ret && h2fmi_page_status(*status) >= 0) { ++ if (*status == PPN_ST_ERASED && memchr_inv(b->page, 0xff, H2FMI_PAGE_RAW)) ++ ret = -EIO; ++ else ++ h2fmi_split_page(b->page, data, oob); ++ } else if (!ret && *status != PPN_ST_RETIRED) { ++ ret = -EIO; ++ } ++ mutex_unlock(&b->lock); ++ return ret; ++} ++ ++static int h2fmi_ppn_program(void *priv, unsigned int bus, unsigned int cau, ++ unsigned int block, unsigned int page, ++ const u8 *data, const u8 *oob) ++{ ++ struct h2fmi *fmi = priv; ++ ++ if (bus >= H2FMI_BUSES) ++ return -EINVAL; ++ return h2fmi_program_page(fmi, &fmi->bus[bus], cau, block, page, data, oob); ++} ++ ++static const struct apple_ppn_ops h2fmi_ppn_ops = { ++ .read_page = h2fmi_ppn_read, ++ .program_page = h2fmi_ppn_program, ++}; ++ ++static int h2fmi_register_sftl(struct h2fmi *fmi) ++{ ++ const struct h2fmi_geometry *g = &fmi->bus[0].geometry; ++ struct apple_ppn provider = { ++ .dev = fmi->dev, ++ .ops = &h2fmi_ppn_ops, ++ .priv = fmi, ++ .buses = H2FMI_BUSES, ++ .caus = g->caus, ++ .blocks = g->blocks, ++ .pages = g->pages, ++ }; ++ ++ /* This recovery implementation supports the measured 64 GB profile. */ ++ if (!IS_ENABLED(CONFIG_MTD_APPLE_SFTL) || g->caus != 8 || ++ g->blocks != 2088 || g->pages != 128 || g->slc_pages != 64) ++ return 0; ++ fmi->sftl = platform_device_register_data(fmi->dev, "apple-sftl", ++ PLATFORM_DEVID_AUTO, ++ &provider, sizeof(provider)); ++ if (IS_ERR(fmi->sftl)) ++ return PTR_ERR(fmi->sftl); ++ return 0; ++} ++ + /* + * Turn the PPN's status byte into what MTD expects back from _read_oob: a + * count of corrected bitflips, or a negative errno. +@@ -841,6 +922,13 @@ static int h2fmi_probe(struct platform_device *pdev) + } + } + ++ ret = h2fmi_register_sftl(fmi); ++ if (ret) { ++ for (i = 0; i < fmi->ndies; i++) ++ mtd_device_unregister(&fmi->mtd[i]); ++ return ret; ++ } ++ + platform_set_drvdata(pdev, fmi); + dev_info(&pdev->dev, "%u CAUs, %u blocks of %u KiB each\n", + fmi->ndies, fmi->bus[0].geometry.blocks, +@@ -853,6 +941,8 @@ static void h2fmi_remove(struct platform_device *pdev) + struct h2fmi *fmi = platform_get_drvdata(pdev); + int i; + ++ if (fmi->sftl) ++ platform_device_unregister(fmi->sftl); + for (i = 0; i < fmi->ndies; i++) + mtd_device_unregister(&fmi->mtd[i]); + } +diff --git a/include/linux/mtd/apple-ppn.h b/include/linux/mtd/apple-ppn.h +new file mode 100644 +index 000000000000..73fba467d00f +--- /dev/null ++++ b/include/linux/mtd/apple-ppn.h +@@ -0,0 +1,33 @@ ++/* SPDX-License-Identifier: GPL-2.0-only */ ++#ifndef _LINUX_MTD_APPLE_PPN_H ++#define _LINUX_MTD_APPLE_PPN_H ++ ++#include ++ ++struct device; ++ ++#define APPLE_PPN_PAGE_SIZE 16384 ++#define APPLE_PPN_OOB_SIZE 64 ++ ++/* All buffers have canonical data/OOB layout, including context reads. */ ++struct apple_ppn_ops { ++ /* Success means the complete page and metadata passed exact readback. */ ++ int (*program_page)(void *priv, unsigned int bus, unsigned int cau, ++ unsigned int block, unsigned int page, ++ const u8 *data, const u8 *oob); ++ int (*read_page)(void *priv, unsigned int bus, unsigned int cau, ++ unsigned int block, unsigned int page, bool context, ++ u8 *data, u8 *oob, u8 *status); ++}; ++ ++struct apple_ppn { ++ struct device *dev; ++ const struct apple_ppn_ops *ops; ++ void *priv; ++ u16 buses; ++ u16 caus; ++ u16 blocks; ++ u16 pages; ++}; ++ ++#endif diff --git a/kernel/patches/armv7/mtd/0004-mtd-Add-the-Apple-VFL-and-SwissPPNFTL-translation-la.patch b/kernel/patches/armv7/mtd/0004-mtd-Add-the-Apple-VFL-and-SwissPPNFTL-translation-la.patch new file mode 100644 index 00000000..d3d25fa8 --- /dev/null +++ b/kernel/patches/armv7/mtd/0004-mtd-Add-the-Apple-VFL-and-SwissPPNFTL-translation-la.patch @@ -0,0 +1,1840 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Shayne Legault <8741557+shaynelegault@users.noreply.github.com> +Date: Fri, 6 Nov 2026 11:48:19 -0600 +Subject: [PATCH] mtd: Add the Apple VFL and SwissPPNFTL translation layer + +iOS stores its disk on PPN NAND through Apple's VFL and SwissPPNFTL. Recover +the 64 GB layout from the replicated VFL contexts and lane tables, decode +the saved SFTL ranges and replay the journal written after them, including +completed-block summaries and open stripes with holes. Unsupported formats +and incomplete sequences fail before a disk is registered. + +Expose the 64,000,000,000-byte logical medium as the applesftlN block +device. Requests use a preallocated buffer, and a failed storage read stops +further I/O. + +The format rules are ported from Orchard's P105 Python reference. KUnit +tests build complete synthetic media for checkpoint recovery, journal and +trim ordering, replica disagreement, torn summaries and runtime failure; +they contain no captured device data. + +[Orchard: from shaynelegault/orchard#89 ffa74e8 (source e7d118292); + dropped the read budget and lifetime module parameters of the following + source patch; shortened the commit message] + +Assisted-by: LLM +--- + Documentation/driver-api/mtd/apple-sftl.rst | 61 ++ + Documentation/driver-api/mtd/index.rst | 2 +- + drivers/mtd/Kconfig | 21 + + drivers/mtd/Makefile | 4 + + drivers/mtd/apple-sftl-block.c | 234 +++++ + drivers/mtd/apple-sftl-core.c | 949 ++++++++++++++++++ + drivers/mtd/apple-sftl-test.c | 436 ++++++++ + drivers/mtd/apple-sftl.h | 23 + + tools/testing/kunit/configs/apple_sftl.config | 4 + + 9 files changed, 1733 insertions(+), 1 deletion(-) + create mode 100644 Documentation/driver-api/mtd/apple-sftl.rst + create mode 100644 drivers/mtd/apple-sftl-block.c + create mode 100644 drivers/mtd/apple-sftl-core.c + create mode 100644 drivers/mtd/apple-sftl-test.c + create mode 100644 drivers/mtd/apple-sftl.h + create mode 100644 tools/testing/kunit/configs/apple_sftl.config + +diff --git a/Documentation/driver-api/mtd/apple-sftl.rst b/Documentation/driver-api/mtd/apple-sftl.rst +new file mode 100644 +index 000000000000..8295a5a2a84b +--- /dev/null ++++ b/Documentation/driver-api/mtd/apple-sftl.rst +@@ -0,0 +1,61 @@ ++.. SPDX-License-Identifier: GPL-2.0-only ++ ++Apple PPN VFL/SwissPPNFTL reader ++============================== ++ ++The ``apple-sftl`` driver recovers the logical medium above Apple's PPN flash ++mapping. H2FMI supplies complete 16 KiB pages and their 64 metadata bytes ++through an aggregate provider. Context reads use the package's separate SLC ++address view. The controller serializes each complete transaction. ++ ++Supported format ++---------------- ++ ++This implementation supports the measured 64 GB Hynix PPN profile with two ++buses and eight CAUs per bus. Each CAU has 2,088 blocks of 128 pages. Profile ++selection uses discovered geometry. Other layouts return ``-EOPNOTSUPP``. ++ ++Recovery selects four agreeing VFL checkpoint copies and checks each ++replicated lane table. A VFL superblock has an 8,192-unit address stride; ++units are 4 KiB. Blocks with fewer active lanes have an unused address gap ++which the reader rejects. ++ ++SFTL subtype-4 context fragments describe the saved logical ranges. Recovery ++validates the complete context sequence, then applies later data and trim ++records in sequence order. Completed blocks carry summaries at their ends. ++Open blocks are scanned completely because an interrupted stripe may leave ++an erased lane before another completed lane. Summary allocation spans also ++preserve the next sequence floor when it exceeds the last visible record. ++ ++The map has 15,625,016 entries, including sixteen internal keys. The block ++device exposes 15,625,000 entries, or 64,000,000,000 bytes, with 512-byte sector ++reads. Linux's disk name is ``applesftlN``. LwVM and filesystem interpretation ++happen above this logical provider. Encrypted partition contents remain ++ciphertext. ++ ++Failure and lifetime rules ++-------------------------- ++ ++The disk is read-only and the provider has no programming or erase operation. ++Malformed context copies and incomplete journal sequences prevent disk ++registration. A refresh status requires a writer and is reported as an error. ++A failed runtime read stops subsequent reads from this map instance. ++ ++H2FMI unregisters the child before releasing its controller state. The block ++frontend freezes and drains requests during removal. Its request buffer is ++allocated before the disk becomes visible, while blocking requests serialize ++access to that buffer and the provider. ++ ++Tests ++----- ++ ++``tools/testing/kunit/configs/apple_sftl.config`` enables the synthetic medium ++tests. They generate VFL replicas and lane tables, plus an SFTL checkpoint ++followed by completed and open data blocks. Tests cover trim ordering and ++allocation floors. Corrupt replicas and sequence gaps must return an error ++with no disk or usable map. Test inputs contain no captured device bytes. ++ ++The format rules were ported from Orchard's P105 Python reference ++(``ftl_map.py``, ``ftl_load.py`` and ``ftl_log.py``), which was checked against ++native iOS logical reads. A kernel object compile or synthetic test result ++does not establish physical-controller timing or qualify a new device layout. +diff --git a/Documentation/driver-api/mtd/index.rst b/Documentation/driver-api/mtd/index.rst +index 6a4278f409d7..e91da2aa5ad0 100644 +--- a/Documentation/driver-api/mtd/index.rst ++++ b/Documentation/driver-api/mtd/index.rst +@@ -9,4 +9,4 @@ Memory Technology Device (MTD) + + spi-intel + nand_ecc +- spi-nor ++ spi-nor apple-sftl +diff --git a/drivers/mtd/Kconfig b/drivers/mtd/Kconfig +index 796a2eccbef0..155136a36a91 100644 +--- a/drivers/mtd/Kconfig ++++ b/drivers/mtd/Kconfig +@@ -206,6 +206,27 @@ config MTD_PARTITIONED_MASTER + the parent of the partition device be the master device, rather than + what lies behind the master. + ++config MTD_APPLE_SFTL ++ tristate "Apple PPN read-only VFL/SwissPPNFTL translation" ++ depends on BLOCK ++ help ++ Recover the logical block medium stored in Apple VFL/SwissPPNFTL ++ metadata on the measured 64 GB Hynix PPN layout. The H2FMI driver ++ supplies canonical page reads through its aggregate PPN provider. ++ Unsupported checkpoints, missing journal sequences and refresh ++ requirements stop recovery before a disk is registered. ++ ++ The disk is read-only. This driver performs no program or erase. ++ ++config MTD_APPLE_SFTL_KUNIT_TEST ++ tristate "KUnit tests for Apple VFL/SwissPPNFTL recovery" if !KUNIT_ALL_TESTS ++ depends on KUNIT && MTD_APPLE_SFTL ++ default KUNIT_ALL_TESTS ++ help ++ Synthesize complete VFL/SFTL media and exercise checkpoint recovery, ++ native block summaries and open-block journal replay. Malformed ++ replicas and unsupported layouts must fail without storage writes. ++ + source "drivers/mtd/chips/Kconfig" + + source "drivers/mtd/maps/Kconfig" +diff --git a/drivers/mtd/Makefile b/drivers/mtd/Makefile +index 593d0593a038..fa6180a62577 100644 +--- a/drivers/mtd/Makefile ++++ b/drivers/mtd/Makefile +@@ -31,3 +31,7 @@ obj-y += chips/ lpddr/ maps/ devices/ nand/ tests/ + obj-$(CONFIG_MTD_SPI_NOR) += spi-nor/ + obj-$(CONFIG_MTD_UBI) += ubi/ + obj-$(CONFIG_MTD_HYPERBUS) += hyperbus/ ++ ++obj-$(CONFIG_MTD_APPLE_SFTL) += apple-sftl.o ++apple-sftl-y := apple-sftl-core.o apple-sftl-block.o ++obj-$(CONFIG_MTD_APPLE_SFTL_KUNIT_TEST) += apple-sftl-test.o +diff --git a/drivers/mtd/apple-sftl-block.c b/drivers/mtd/apple-sftl-block.c +new file mode 100644 +index 000000000000..86d02921b153 +--- /dev/null ++++ b/drivers/mtd/apple-sftl-block.c +@@ -0,0 +1,234 @@ ++// SPDX-License-Identifier: GPL-2.0-only ++/* Read-only block frontend for Apple's VFL/SwissPPNFTL logical medium. */ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "apple-sftl.h" ++ ++#define SFTL_BLOCK_NAME "apple-sftl" ++#define SFTL_DISK_MINORS 16 ++#define SFTL_LOGICAL_BYTES 4096U ++ ++struct sftl_disk { ++ struct apple_sftl *ftl; ++ struct blk_mq_tag_set tags; ++ struct gendisk *disk; ++ struct mutex io_lock; /* serializes requests using buffer */ ++ u8 *buffer; ++ unsigned int id; ++ bool dead; ++}; ++ ++static DEFINE_IDA(sftl_disk_ids); ++static int sftl_major; ++ ++/* The blocking queue and this lock keep the preallocated buffer exclusive. */ ++static int sftl_read_request(struct sftl_disk *sftl, struct request *request) ++{ ++ struct req_iterator iterator; ++ struct bio_vec bvec; ++ u64 position = (u64)blk_rq_pos(request) << SECTOR_SHIFT; ++ u32 cached = U32_MAX; ++ ++ rq_for_each_segment(bvec, request, iterator) { ++ unsigned int done = 0; ++ ++ while (done < bvec.bv_len) { ++ u32 logical = position >> 12; ++ unsigned int offset = position & (SFTL_LOGICAL_BYTES - 1); ++ unsigned int length = min(bvec.bv_len - done, ++ SFTL_LOGICAL_BYTES - offset); ++ void *mapped; ++ int ret; ++ ++ if (logical != cached) { ++ ret = apple_sftl_read(sftl->ftl, logical, sftl->buffer); ++ if (ret) ++ return ret; ++ cached = logical; ++ } ++ mapped = kmap_local_page(bvec.bv_page); ++ memcpy(mapped + bvec.bv_offset + done, ++ sftl->buffer + offset, length); ++ kunmap_local(mapped); ++ done += length; ++ position += length; ++ } ++ } ++ return 0; ++} ++ ++static blk_status_t sftl_queue_rq(struct blk_mq_hw_ctx *hctx, ++ const struct blk_mq_queue_data *data) ++{ ++ struct request *request = data->rq; ++ struct sftl_disk *sftl = request->q->queuedata; ++ sector_t capacity = get_capacity(sftl->disk); ++ blk_status_t status; ++ int ret = 0; ++ ++ blk_mq_start_request(request); ++ mutex_lock(&sftl->io_lock); ++ if (sftl->dead) { ++ ret = -ENODEV; ++ } else if (req_op(request) == REQ_OP_FLUSH) { ++ /* A read-only device has no buffered writes to flush. */ ++ } else if (req_op(request) != REQ_OP_READ) { ++ ret = -EOPNOTSUPP; ++ } else if (blk_rq_pos(request) > capacity || ++ blk_rq_sectors(request) > capacity - blk_rq_pos(request)) { ++ ret = -EIO; ++ } else { ++ ret = sftl_read_request(sftl, request); ++ } ++ mutex_unlock(&sftl->io_lock); ++ status = errno_to_blk_status(ret); ++ blk_mq_end_request(request, status); ++ return BLK_STS_OK; ++} ++ ++static const struct blk_mq_ops sftl_mq_ops = { ++ .queue_rq = sftl_queue_rq, ++}; ++ ++static const struct block_device_operations sftl_block_ops = { ++ .owner = THIS_MODULE, ++}; ++ ++static int sftl_disk_probe(struct platform_device *pdev) ++{ ++ struct queue_limits limits = { ++ .logical_block_size = SECTOR_SIZE, ++ .physical_block_size = SFTL_LOGICAL_BYTES, ++ .max_hw_sectors = 128, ++ }; ++ struct apple_ppn *provider = dev_get_platdata(&pdev->dev); ++ struct apple_sftl_info info; ++ struct sftl_disk *sftl; ++ struct gendisk *disk; ++ int ret, id; ++ ++ if (!provider) ++ return -EINVAL; ++ sftl = kzalloc(sizeof(*sftl), GFP_KERNEL); ++ if (!sftl) ++ return -ENOMEM; ++ mutex_init(&sftl->io_lock); ++ sftl->buffer = kmalloc(SFTL_LOGICAL_BYTES, GFP_KERNEL); ++ if (!sftl->buffer) { ++ ret = -ENOMEM; ++ goto free_state; ++ } ++ ret = apple_sftl_load(provider, &sftl->ftl); ++ if (ret) ++ goto free_buffer; ++ apple_sftl_get_info(sftl->ftl, &info); ++ if (!info.disk_pages || info.disk_pages > info.logical_pages) { ++ ret = -EINVAL; ++ goto free_ftl; ++ } ++ id = ida_alloc_range(&sftl_disk_ids, 0, MINORMASK / SFTL_DISK_MINORS, ++ GFP_KERNEL); ++ if (id < 0) { ++ ret = id; ++ goto free_ftl; ++ } ++ sftl->id = id; ++ ret = blk_mq_alloc_sq_tag_set(&sftl->tags, &sftl_mq_ops, 8, ++ BLK_MQ_F_SHOULD_MERGE | BLK_MQ_F_BLOCKING); ++ if (ret) ++ goto free_id; ++ disk = blk_mq_alloc_disk(&sftl->tags, &limits, sftl); ++ if (IS_ERR(disk)) { ++ ret = PTR_ERR(disk); ++ goto free_tags; ++ } ++ sftl->disk = disk; ++ disk->major = sftl_major; ++ disk->first_minor = id * SFTL_DISK_MINORS; ++ disk->minors = SFTL_DISK_MINORS; ++ disk->fops = &sftl_block_ops; ++ disk->private_data = sftl; ++ snprintf(disk->disk_name, DISK_NAME_LEN, "applesftl%d", id); ++ set_capacity(disk, (sector_t)info.disk_pages * (SFTL_LOGICAL_BYTES / SECTOR_SIZE)); ++ set_disk_ro(disk, true); ++ platform_set_drvdata(pdev, sftl); ++ ret = device_add_disk(&pdev->dev, disk, NULL); ++ if (ret) ++ goto put_disk; ++ dev_info(&pdev->dev, "%s: read-only, %u logical pages, sequence %llu\n", ++ disk->disk_name, info.disk_pages, info.sequence); ++ return 0; ++ ++put_disk: ++ platform_set_drvdata(pdev, NULL); ++ put_disk(disk); ++free_tags: ++ blk_mq_free_tag_set(&sftl->tags); ++free_id: ++ ida_free(&sftl_disk_ids, sftl->id); ++free_ftl: ++ apple_sftl_free(sftl->ftl); ++free_buffer: ++ kfree(sftl->buffer); ++free_state: ++ kfree(sftl); ++ return ret; ++} ++ ++static void sftl_disk_remove(struct platform_device *pdev) ++{ ++ struct sftl_disk *sftl = platform_get_drvdata(pdev); ++ ++ mutex_lock(&sftl->io_lock); ++ sftl->dead = true; ++ mutex_unlock(&sftl->io_lock); ++ /* del_gendisk freezes and drains the queue before the provider is freed. */ ++ del_gendisk(sftl->disk); ++ put_disk(sftl->disk); ++ blk_mq_free_tag_set(&sftl->tags); ++ ida_free(&sftl_disk_ids, sftl->id); ++ apple_sftl_free(sftl->ftl); ++ kfree(sftl->buffer); ++ kfree(sftl); ++} ++ ++static struct platform_driver sftl_driver = { ++ .probe = sftl_disk_probe, ++ .remove = sftl_disk_remove, ++ .driver = { ++ .name = SFTL_BLOCK_NAME, ++ }, ++}; ++ ++static int __init sftl_init(void) ++{ ++ int ret; ++ ++ sftl_major = register_blkdev(0, SFTL_BLOCK_NAME); ++ if (sftl_major < 0) ++ return sftl_major; ++ ret = platform_driver_register(&sftl_driver); ++ if (ret) ++ unregister_blkdev(sftl_major, SFTL_BLOCK_NAME); ++ return ret; ++} ++module_init(sftl_init); ++ ++static void __exit sftl_exit(void) ++{ ++ platform_driver_unregister(&sftl_driver); ++ unregister_blkdev(sftl_major, SFTL_BLOCK_NAME); ++ ida_destroy(&sftl_disk_ids); ++} ++module_exit(sftl_exit); ++ ++MODULE_DESCRIPTION("Apple VFL/SwissPPNFTL read-only block driver"); ++MODULE_LICENSE("GPL"); ++MODULE_ALIAS("platform:" SFTL_BLOCK_NAME); +diff --git a/drivers/mtd/apple-sftl-core.c b/drivers/mtd/apple-sftl-core.c +new file mode 100644 +index 000000000000..ba90916f2efe +--- /dev/null ++++ b/drivers/mtd/apple-sftl-core.c +@@ -0,0 +1,949 @@ ++// SPDX-License-Identifier: GPL-2.0-only ++/* ++ * Apple VFL/SwissPPNFTL read-only recovery for the measured 64 GB PPN layout. ++ * ++ * Format and replay rules are ported from Orchard's P105 Python reference ++ * (ftl_map.py, ftl_load.py and ftl_log.py). That implementation was checked ++ * against native iOS logical reads. This code contains no captured firmware ++ * or device data. Unsupported formats fail before a block device is created. ++ */ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "apple-sftl.h" ++ ++#define SFTL_LPAGES 15625016U ++#define SFTL_DISK_PAGES 15625000U ++#define SFTL_BLOCKS 2088U ++#define SFTL_SUBBLOCKS 2086U ++#define SFTL_LANES 16U ++#define SFTL_STRIDE 8192U ++#define SFTL_PROVIDER (SFTL_SUBBLOCKS * SFTL_STRIDE) ++#define SFTL_UNMAPPED U32_MAX ++#define SFTL_SEQUENCE GENMASK_ULL(47, 0) ++#define SFTL_MAX_CONTEXTS 16U ++#define SFTL_META 16U ++#define SFTL_CHUNK 4096U ++#define SFTL_TRIM 0xffff0001U ++ ++struct sftl_run { ++ struct rb_node node; ++ u32 start, end, provider; ++}; ++ ++/* stamp packs the native 48-bit sequence with its flags and record type. */ ++struct sftl_record { ++ u64 stamp; ++ u32 key, address; ++}; ++ ++struct sftl_records { ++ struct sftl_record *items; ++ u32 count, capacity; ++}; ++ ++struct sftl_member { ++ u16 block; ++ u8 lane; ++}; ++ ++struct apple_sftl { ++ struct apple_ppn *ppn; ++ struct rb_root runs; ++ u32 nr_runs; ++ u16 lanes[SFTL_BLOCKS]; ++ DECLARE_BITMAP(reserved, SFTL_BLOCKS); ++ u64 checkpoint_sequence, sequence; ++ u8 *page; ++ u8 oob[APPLE_PPN_OOB_SIZE]; ++ struct mutex lock; /* serializes reads and the shared page buffer */ ++ bool failed; ++}; ++ ++static u64 sftl_sequence(const u8 *meta) ++{ ++ return get_unaligned_le64(meta) >> 16; ++} ++ ++static u64 sftl_stamp(const u8 *meta) ++{ ++ return sftl_sequence(meta) | (u64)meta[1] << 48 | (u64)meta[0] << 56; ++} ++ ++static unsigned int sftl_kind(const struct sftl_record *record) ++{ ++ return record->stamp >> 56; ++} ++ ++static unsigned int sftl_flags(const struct sftl_record *record) ++{ ++ return (record->stamp >> 48) & 0xff; ++} ++ ++static bool sftl_erased(const void *data, size_t size) ++{ ++ return !memchr_inv(data, 0xff, size); ++} ++ ++static int sftl_read_page(struct apple_sftl *sftl, unsigned int bus, ++ unsigned int cau, unsigned int block, ++ unsigned int page, bool context, u8 *data, u8 *oob) ++{ ++ u8 status; ++ int ret; ++ ++ ret = sftl->ppn->ops->read_page(sftl->ppn->priv, bus, cau, block, ++ page, context, data, oob, &status); ++ if (ret) ++ return ret; ++ /* Refresh requires a writer. A reader must not hide that requirement. */ ++ if (status != 0x40 && status != 0x49) ++ return -EIO; ++ if (status == 0x49 && (!sftl_erased(data, APPLE_PPN_PAGE_SIZE) || ++ !sftl_erased(oob, APPLE_PPN_OOB_SIZE))) ++ return -EBADMSG; ++ return 0; ++} ++ ++static int sftl_translate(struct apple_sftl *sftl, u32 address, ++ unsigned int *bus, unsigned int *cau, ++ unsigned int *block, unsigned int *page, ++ unsigned int *chunk) ++{ ++ u32 offset; ++ unsigned int lane, wanted, count; ++ u16 bitmap; ++ ++ if (address >= SFTL_PROVIDER) ++ return -ERANGE; ++ *chunk = address & 3; ++ *block = address / SFTL_STRIDE; ++ offset = (address % SFTL_STRIDE) / 4; ++ bitmap = sftl->lanes[*block]; ++ count = hweight16(bitmap); ++ if (!count || offset / count >= 128) ++ return -ERANGE; ++ *page = offset / count; ++ wanted = offset % count; ++ for (lane = 0; lane < SFTL_LANES; lane++) { ++ if (!(bitmap & BIT(lane))) ++ continue; ++ if (!wanted--) ++ break; ++ } ++ *bus = lane & 1; ++ *cau = lane >> 1; ++ return 0; ++} ++ ++static int sftl_read_address(struct apple_sftl *sftl, u32 address, ++ unsigned int *chunk) ++{ ++ unsigned int bus, cau, block, page; ++ int ret; ++ ++ ret = sftl_translate(sftl, address, &bus, &cau, &block, &page, chunk); ++ if (ret) ++ return ret; ++ return sftl_read_page(sftl, bus, cau, block, page, false, ++ sftl->page, sftl->oob); ++} ++ ++static struct sftl_run *sftl_find(struct apple_sftl *sftl, u32 logical) ++{ ++ struct rb_node *node = sftl->runs.rb_node; ++ ++ while (node) { ++ struct sftl_run *run = rb_entry(node, struct sftl_run, node); ++ ++ if (logical < run->start) ++ node = node->rb_left; ++ else if (logical >= run->end) ++ node = node->rb_right; ++ else ++ return run; ++ } ++ return NULL; ++} ++ ++static void sftl_insert(struct apple_sftl *sftl, struct sftl_run *run) ++{ ++ struct rb_node **link = &sftl->runs.rb_node, *parent = NULL; ++ ++ while (*link) { ++ struct sftl_run *entry = rb_entry(*link, struct sftl_run, node); ++ ++ parent = *link; ++ link = run->start < entry->start ? &parent->rb_left : ++ &parent->rb_right; ++ } ++ rb_link_node(&run->node, parent, link); ++ rb_insert_color(&run->node, &sftl->runs); ++ sftl->nr_runs++; ++} ++ ++static int sftl_add_run(struct apple_sftl *sftl, u32 start, u32 end, u32 provider) ++{ ++ struct sftl_run *run; ++ ++ if (start >= end || end > SFTL_LPAGES || ++ (provider != SFTL_UNMAPPED && ++ (provider >= SFTL_PROVIDER || end - start > SFTL_PROVIDER - provider))) ++ return -EBADMSG; ++ run = kmalloc(sizeof(*run), GFP_KERNEL); ++ if (!run) ++ return -ENOMEM; ++ run->start = start; ++ run->end = end; ++ run->provider = provider; ++ sftl_insert(sftl, run); ++ return 0; ++} ++ ++static int sftl_split(struct apple_sftl *sftl, u32 point) ++{ ++ struct sftl_run *left = sftl_find(sftl, point), *right; ++ ++ if (!left) ++ return -EBADMSG; ++ if (left->start == point) ++ return 0; ++ right = kmalloc(sizeof(*right), GFP_KERNEL); ++ if (!right) ++ return -ENOMEM; ++ right->start = point; ++ right->end = left->end; ++ right->provider = left->provider == SFTL_UNMAPPED ? SFTL_UNMAPPED : ++ left->provider + point - left->start; ++ left->end = point; ++ sftl_insert(sftl, right); ++ return 0; ++} ++ ++static bool sftl_contiguous(struct sftl_run *left, struct sftl_run *right) ++{ ++ return left->end == right->start && ++ ((left->provider == SFTL_UNMAPPED && right->provider == SFTL_UNMAPPED) || ++ (left->provider != SFTL_UNMAPPED && right->provider != SFTL_UNMAPPED && ++ left->provider + left->end - left->start == right->provider)); ++} ++ ++static void sftl_merge(struct apple_sftl *sftl, struct sftl_run *run) ++{ ++ struct rb_node *node = rb_prev(&run->node); ++ struct sftl_run *neighbor; ++ ++ if (node) { ++ neighbor = rb_entry(node, struct sftl_run, node); ++ if (sftl_contiguous(neighbor, run)) { ++ neighbor->end = run->end; ++ rb_erase(&run->node, &sftl->runs); ++ kfree(run); ++ sftl->nr_runs--; ++ run = neighbor; ++ } ++ } ++ node = rb_next(&run->node); ++ if (!node) ++ return; ++ neighbor = rb_entry(node, struct sftl_run, node); ++ if (sftl_contiguous(run, neighbor)) { ++ run->end = neighbor->end; ++ rb_erase(node, &sftl->runs); ++ kfree(neighbor); ++ sftl->nr_runs--; ++ } ++} ++ ++static int sftl_replace(struct apple_sftl *sftl, u32 start, u32 length, u32 address) ++{ ++ struct sftl_run *run, *new; ++ struct rb_node *node, *next; ++ u32 end; ++ int ret; ++ ++ if (!length || start >= SFTL_LPAGES || length > SFTL_LPAGES - start || ++ (address != SFTL_UNMAPPED && ++ (address >= SFTL_PROVIDER || length > SFTL_PROVIDER - address))) ++ return -EBADMSG; ++ end = start + length; ++ ret = sftl_split(sftl, start); ++ if (ret) ++ return ret; ++ if (end < SFTL_LPAGES) { ++ ret = sftl_split(sftl, end); ++ if (ret) ++ return ret; ++ } ++ new = kmalloc(sizeof(*new), GFP_KERNEL); ++ if (!new) ++ return -ENOMEM; ++ run = sftl_find(sftl, start); ++ for (node = &run->node; node; node = next) { ++ run = rb_entry(node, struct sftl_run, node); ++ if (run->start >= end) ++ break; ++ next = rb_next(node); ++ rb_erase(node, &sftl->runs); ++ kfree(run); ++ sftl->nr_runs--; ++ } ++ new->start = start; ++ new->end = end; ++ new->provider = address; ++ sftl_insert(sftl, new); ++ sftl_merge(sftl, new); ++ return 0; ++} ++ ++static int sftl_record_add(struct sftl_records *records, u64 stamp, ++ u32 key, u32 address) ++{ ++ struct sftl_record *new; ++ u32 capacity; ++ ++ if (records->count == SFTL_PROVIDER) ++ return -E2BIG; ++ if (records->count == records->capacity) { ++ capacity = min_t(u32, max(records->capacity * 2, 256U), SFTL_PROVIDER); ++ new = kvrealloc(records->items, array_size(capacity, sizeof(*new)), ++ GFP_KERNEL); ++ if (!new) ++ return -ENOMEM; ++ records->items = new; ++ records->capacity = capacity; ++ } ++ records->items[records->count++] = (struct sftl_record) { ++ .stamp = stamp, .key = key, .address = address, ++ }; ++ return 0; ++} ++ ++static int sftl_record_compare(const void *a, const void *b) ++{ ++ u64 left = ((const struct sftl_record *)a)->stamp & SFTL_SEQUENCE; ++ u64 right = ((const struct sftl_record *)b)->stamp & SFTL_SEQUENCE; ++ ++ return left < right ? -1 : left > right; ++} ++ ++static int sftl_vfl(struct apple_sftl *sftl) ++{ ++ struct sftl_member members[8]; ++ u8 list[32], best_oob[64], best_members = 0; ++ u8 *best, *table; ++ u32 best_age = U32_MAX; ++ unsigned int member, page, latest_page = 0, lane, block; ++ int ret; ++ ++ best = kmalloc(APPLE_PPN_PAGE_SIZE, GFP_KERNEL); ++ table = kmalloc(APPLE_PPN_PAGE_SIZE + APPLE_PPN_OOB_SIZE, GFP_KERNEL); ++ if (!best || !table) { ++ ret = -ENOMEM; ++ goto out; ++ } ++ ret = sftl_read_page(sftl, 0, 0, 2087, 0, true, sftl->page, sftl->oob); ++ if (ret) ++ goto out; ++ if (get_unaligned_le32(sftl->oob) != 0x00ffff20) { ++ ret = -EBADMSG; ++ goto out; ++ } ++ memcpy(list, sftl->page + 0x100, sizeof(list)); ++ for (member = 0; member < 8; member++) { ++ unsigned int other; ++ ++ members[member].block = get_unaligned_le16(list + member * 4); ++ members[member].lane = list[member * 4 + 2]; ++ if (members[member].block < 2056 || members[member].block >= SFTL_BLOCKS || ++ members[member].lane >= SFTL_LANES || list[member * 4 + 3]) { ++ ret = -EBADMSG; ++ goto out; ++ } ++ for (other = 0; other < member; other++) ++ if (members[other].block == members[member].block && ++ members[other].lane == members[member].lane) { ++ ret = -EBADMSG; ++ goto out; ++ } ++ } ++ for (member = 0; member < 8; member++) { ++ for (page = 0; page < 64; page++) { ++ u32 age; ++ ++ ret = sftl_read_page(sftl, members[member].lane & 1, ++ members[member].lane >> 1, members[member].block, ++ page, true, sftl->page, sftl->oob); ++ if (ret) ++ goto out; ++ if (!page && memcmp(sftl->page + 0x100, list, sizeof(list))) { ++ ret = -EBADMSG; ++ goto out; ++ } ++ if (get_unaligned_le32(sftl->oob) != 0x00ffff20) ++ continue; ++ age = get_unaligned_le32(sftl->oob + 4); ++ if (age < BIT(31)) { ++ ret = -EOPNOTSUPP; ++ goto out; ++ } ++ if (age > best_age) ++ continue; ++ if (!best_members || age < best_age) { ++ best_age = age; ++ latest_page = page; ++ memcpy(best, sftl->page, APPLE_PPN_PAGE_SIZE); ++ memcpy(best_oob, sftl->oob, sizeof(best_oob)); ++ best_members = BIT(member); ++ } else { ++ if (page != latest_page || best_members & BIT(member) || ++ memcmp(best, sftl->page, APPLE_PPN_PAGE_SIZE) || ++ memcmp(best_oob, sftl->oob, sizeof(best_oob))) { ++ ret = -EBADMSG; ++ goto out; ++ } ++ best_members |= BIT(member); ++ } ++ } ++ } ++ if ((best_members != 0x0f && best_members != 0xf0) || ++ get_unaligned_le16(best + 10) != 16 || ++ get_unaligned_le16(best + 12) != SFTL_SUBBLOCKS || ++ get_unaligned_le16(best + 14) != SFTL_SUBBLOCKS) { ++ ret = -EBADMSG; ++ goto out; ++ } ++ for (lane = 0; lane < SFTL_LANES; lane++) { ++ u16 reference = get_unaligned_le16(best + 0x200 + lane * 32); ++ unsigned int group = reference >> 15; ++ ++ page = reference & 0x7fff; ++ if (page >= 64) { ++ ret = -EBADMSG; ++ goto out; ++ } ++ for (member = group * 4; member < group * 4 + 4; member++) { ++ ret = sftl_read_page(sftl, members[member].lane & 1, ++ members[member].lane >> 1, members[member].block, ++ page, true, sftl->page, sftl->oob); ++ if (ret) ++ goto out; ++ if (sftl->oob[0] != 0x20 || sftl->oob[1] != lane || ++ sftl->oob[2] || sftl->oob[3]) { ++ ret = -EBADMSG; ++ goto out; ++ } ++ if (member == group * 4) { ++ memcpy(table, sftl->page, APPLE_PPN_PAGE_SIZE); ++ memcpy(table + APPLE_PPN_PAGE_SIZE, sftl->oob, sizeof(sftl->oob)); ++ } else if (memcmp(table, sftl->page, APPLE_PPN_PAGE_SIZE) || ++ memcmp(table + APPLE_PPN_PAGE_SIZE, ++ sftl->oob, sizeof(sftl->oob))) { ++ ret = -EBADMSG; ++ goto out; ++ } ++ } ++ for (block = 0; block < SFTL_BLOCKS; block++) ++ if (!(get_unaligned_le16(table + block * 2) & BIT(15))) ++ sftl->lanes[block] |= BIT(lane); ++ } ++ ret = 0; ++out: ++ kfree(table); ++ kfree(best); ++ return ret; ++} ++ ++static int sftl_context_names(struct apple_sftl *sftl, const u8 *payload, ++ u16 *pending, u32 *count) ++{ ++ u32 names = get_unaligned_le32(payload), i; ++ ++ if (!names || names > SFTL_MAX_CONTEXTS) ++ return -EBADMSG; ++ for (i = 0; i < names; i++) { ++ u32 name = get_unaligned_le32(payload + 4 + i * 4); ++ ++ if (!name || name >= SFTL_SUBBLOCKS) ++ return -EBADMSG; ++ if (test_bit(name, sftl->reserved)) ++ continue; ++ if (*count == SFTL_MAX_CONTEXTS) ++ return -E2BIG; ++ __set_bit(name, sftl->reserved); ++ pending[(*count)++] = name; ++ } ++ return 0; ++} ++ ++static int sftl_context(struct apple_sftl *sftl, const u8 *heads) ++{ ++ struct sftl_records records = {}; ++ u16 pending[SFTL_MAX_CONTEXTS]; ++ u32 count = 0, block, at, i, first = 0, cursor = 0, chunk; ++ u64 previous = 0, fragment_sequence = 0; ++ int ret = -EBADMSG; ++ ++ for (block = 1; block < SFTL_SUBBLOCKS; block++) { ++ if (heads[block * SFTL_META] != 0x1f) ++ continue; ++ if (count == SFTL_MAX_CONTEXTS) ++ goto out; ++ pending[count++] = block; ++ __set_bit(block, sftl->reserved); ++ } ++ for (at = 0; at < count; at++) { ++ u32 position, pages; ++ ++ block = pending[at]; ++ pages = hweight16(sftl->lanes[block]) * 128; ++ if (!pages) { ++ ret = -EBADMSG; ++ goto out; ++ } ++ for (position = 0; position < pages; position++) { ++ ret = sftl_read_address(sftl, block * SFTL_STRIDE + position * 4, &chunk); ++ if (ret) ++ goto out; ++ if (sftl_erased(sftl->oob, sizeof(sftl->oob))) { ++ ret = sftl_erased(sftl->page, APPLE_PPN_PAGE_SIZE) ? 0 : -EBADMSG; ++ if (ret) ++ goto out; ++ break; ++ } ++ for (chunk = 0; chunk < 4; chunk++) { ++ const u8 *meta = sftl->oob + chunk * SFTL_META; ++ const u8 *payload = sftl->page + chunk * SFTL_CHUNK; ++ ++ if (sftl_erased(meta, SFTL_META)) ++ continue; ++ if (meta[0] != 0x1f || !meta[1] || meta[1] > 6) { ++ ret = -EBADMSG; ++ goto out; ++ } ++ if (meta[1] == 1 && !chunk) { ++ ret = sftl_context_names(sftl, payload, pending, &count); ++ if (ret) ++ goto out; ++ } ++ ret = sftl_record_add(&records, sftl_stamp(meta), 0, ++ block * SFTL_STRIDE + position * 4 + chunk); ++ if (ret) ++ goto out; ++ } ++ } ++ } ++ ret = -EBADMSG; ++ if (!records.count) ++ goto out; ++ sort(records.items, records.count, sizeof(*records.items), sftl_record_compare, NULL); ++ if (sftl_flags(&records.items[0]) != 1) ++ goto out; ++ for (i = 1; i < records.count; i++) ++ if (sftl_flags(&records.items[i]) == 1 && sftl_flags(&records.items[i - 1]) != 1) ++ first = i; ++ if (sftl_flags(&records.items[records.count - 1]) != 4) ++ goto out; ++ for (i = first; i < records.count; i++) { ++ const struct sftl_record *record = &records.items[i]; ++ u64 sequence = record->stamp & SFTL_SEQUENCE; ++ const u8 *payload; ++ u32 offset, value, length, names; ++ ++ if (i != first && sequence != previous + 1) ++ goto out; ++ previous = sequence; ++ ret = sftl_read_address(sftl, record->address, &chunk); ++ if (ret) ++ goto out; ++ ret = -EBADMSG; ++ if (sftl_stamp(sftl->oob + chunk * SFTL_META) != record->stamp) ++ goto out; ++ payload = sftl->page + chunk * SFTL_CHUNK; ++ if (i == first) { ++ names = get_unaligned_le32(payload); ++ if (!names || names > SFTL_MAX_CONTEXTS) ++ goto out; ++ for (offset = 0; offset < names; offset++) { ++ value = get_unaligned_le32(payload + 4 + offset * 4); ++ if (value >= SFTL_BLOCKS || !test_bit(value, sftl->reserved)) ++ goto out; ++ } ++ } ++ if (sftl_flags(record) != 4) ++ continue; ++ if (fragment_sequence && sequence != fragment_sequence + 1) ++ goto out; ++ fragment_sequence = sequence; ++ value = get_unaligned_le32(payload + 4); ++ if (value == U32_MAX) ++ continue; ++ if (value != 0xfffffff0 || get_unaligned_le32(payload) != cursor) ++ goto out; ++ for (offset = 8; offset < SFTL_CHUNK; offset += 8) { ++ value = get_unaligned_le32(payload + offset); ++ length = get_unaligned_le32(payload + offset + 4); ++ if (value == U32_MAX) ++ break; ++ if (!length || length > SFTL_LPAGES - cursor) ++ goto out; ++ if (value == BIT(fls(SFTL_PROVIDER * 2 - 1) - 1) - 1) ++ value = SFTL_UNMAPPED; ++ ret = sftl_add_run(sftl, cursor, cursor + length, value); ++ if (ret) ++ goto out; ++ cursor += length; ++ ret = -EBADMSG; ++ } ++ } ++ if (cursor != SFTL_LPAGES || !fragment_sequence) ++ goto out; ++ sftl->checkpoint_sequence = fragment_sequence; ++ sftl->sequence = fragment_sequence; ++ ret = 0; ++out: ++ kvfree(records.items); ++ return ret; ++} ++ ++static int sftl_add_journal(struct sftl_records *journal, const u8 *meta, ++ u32 address, u64 checkpoint) ++{ ++ u64 sequence = sftl_sequence(meta); ++ ++ if (sequence <= checkpoint || sftl_erased(meta, SFTL_META)) ++ return 0; ++ if ((meta[0] != 1 && meta[0] != 2) || ++ get_unaligned_le32(meta + 12) != sequence >> 16 || ++ (meta[0] == 1 ? meta[1] > 1 : meta[1] != 0)) ++ return -EBADMSG; ++ return sftl_record_add(journal, sftl_stamp(meta), get_unaligned_le32(meta + 8), address); ++} ++ ++static int sftl_footer(struct apple_sftl *sftl, const u8 *head, u32 block, ++ u32 pages, struct sftl_records *journal, u64 *floor) ++{ ++ u8 footer_meta[16]; ++ u8 *footer; ++ u32 chunks, span, index, position = 0, relative_end = 0; ++ u64 base; ++ unsigned int chunk; ++ bool terminated = false; ++ int ret; ++ ++ memcpy(footer_meta, sftl->oob, sizeof(footer_meta)); ++ base = sftl_sequence(footer_meta); ++ chunks = get_unaligned_le16(footer_meta + 12); ++ span = get_unaligned_le32(footer_meta + 8); ++ if (chunks < 4 || chunks > 32 || chunks % 4 || footer_meta[1] >> 1 != chunks || ++ get_unaligned_le16(footer_meta + 14) != 0xffff || ++ base != sftl_sequence(head) || !span || base + span > SFTL_SEQUENCE) ++ return -EBADMSG; ++ *floor = max(*floor, base + span - 1); ++ if (base + span - 1 <= sftl->checkpoint_sequence) ++ return 0; ++ footer = kmalloc(chunks * SFTL_CHUNK, GFP_KERNEL); ++ if (!footer) ++ return -ENOMEM; ++ for (index = 0; index < chunks / 4; index++) { ++ ret = sftl_read_address(sftl, block * SFTL_STRIDE + ++ (pages - chunks / 4 + index) * 4, &chunk); ++ if (ret) ++ goto out; ++ for (chunk = 0; chunk < 4; chunk++) ++ if (memcmp(footer_meta, sftl->oob + chunk * SFTL_META, SFTL_META)) { ++ ret = -EBADMSG; ++ goto out; ++ } ++ memcpy(footer + index * APPLE_PPN_PAGE_SIZE, sftl->page, APPLE_PPN_PAGE_SIZE); ++ } ++ for (index = 0; index < chunks * SFTL_CHUNK; index += 16) { ++ u32 relative = get_unaligned_le32(footer + index); ++ u32 key = get_unaligned_le32(footer + index + 8); ++ u32 length = get_unaligned_le32(footer + index + 12), offset; ++ ++ if (!length) { ++ terminated = true; ++ break; ++ } ++ if (relative < relative_end || length > U32_MAX - relative || ++ base + relative + length > SFTL_SEQUENCE + 1 || ++ length > pages * 4 - chunks - position) { ++ ret = -EBADMSG; ++ goto out; ++ } ++ relative_end = relative + length; ++ for (offset = 0; offset < length; offset++, position++) { ++ u64 sequence = base + relative + offset; ++ u32 actual_key = key >= 0xffff0000 ? key : key + offset; ++ u32 address = block * SFTL_STRIDE + position; ++ u8 meta[16]; ++ ++ if (sequence <= sftl->checkpoint_sequence) ++ continue; ++ if (key < 0xffff0000 && actual_key < key) { ++ ret = -EBADMSG; ++ goto out; ++ } ++ if (actual_key == SFTL_TRIM) { ++ ret = sftl_read_address(sftl, address, &chunk); ++ if (ret) ++ goto out; ++ memcpy(meta, sftl->oob + chunk * SFTL_META, sizeof(meta)); ++ if (meta[0] != 2 || meta[1] || sftl_sequence(meta) != sequence || ++ get_unaligned_le32(meta + 8) != SFTL_TRIM) { ++ ret = -EBADMSG; ++ goto out; ++ } ++ } else { ++ meta[0] = 1; ++ meta[1] = head[1]; ++ put_unaligned_le64(sequence << 16 | get_unaligned_le16(meta), meta); ++ put_unaligned_le32(actual_key, meta + 8); ++ put_unaligned_le32(sequence >> 16, meta + 12); ++ } ++ ret = sftl_add_journal(journal, meta, address, sftl->checkpoint_sequence); ++ if (ret) ++ goto out; ++ } ++ } ++ ret = terminated && position == pages * 4 - chunks ? 0 : -EBADMSG; ++out: ++ kfree(footer); ++ return ret; ++} ++ ++static int sftl_journal(struct apple_sftl *sftl, const u8 *heads) ++{ ++ struct sftl_records journal = {}; ++ u64 floor = sftl->checkpoint_sequence, expected = floor + 1; ++ u32 block, position, i, pages; ++ unsigned int chunk; ++ int ret; ++ ++ for (block = 1; block < SFTL_SUBBLOCKS; block++) { ++ const u8 *head = heads + block * SFTL_META; ++ ++ if (head[0] != 1 && head[0] != 2) ++ continue; ++ pages = hweight16(sftl->lanes[block]) * 128; ++ ret = sftl_read_address(sftl, block * SFTL_STRIDE + (pages - 1) * 4, &chunk); ++ if (ret) ++ goto out; ++ if (sftl->oob[0] == 0x1c) { ++ ret = sftl_footer(sftl, head, block, pages, &journal, &floor); ++ if (ret) ++ goto out; ++ continue; ++ } ++ if (!sftl_erased(sftl->oob, sizeof(sftl->oob)) || ++ !sftl_erased(sftl->page, APPLE_PPN_PAGE_SIZE)) { ++ ret = -EBADMSG; ++ goto out; ++ } ++ for (position = 0; position < pages; position++) { ++ ret = sftl_read_address(sftl, block * SFTL_STRIDE + position * 4, &chunk); ++ if (ret) ++ goto out; ++ if (sftl_erased(sftl->oob, sizeof(sftl->oob)) && ++ !sftl_erased(sftl->page, APPLE_PPN_PAGE_SIZE)) { ++ ret = -EBADMSG; ++ goto out; ++ } ++ for (chunk = 0; chunk < 4; chunk++) { ++ ret = sftl_add_journal(&journal, sftl->oob + chunk * SFTL_META, ++ block * SFTL_STRIDE + position * 4 + chunk, ++ sftl->checkpoint_sequence); ++ if (ret) ++ goto out; ++ } ++ } ++ } ++ sort(journal.items, journal.count, sizeof(*journal.items), sftl_record_compare, NULL); ++ for (i = 0; i < journal.count; i++) { ++ const struct sftl_record *record = &journal.items[i]; ++ u64 sequence = record->stamp & SFTL_SEQUENCE; ++ ++ ret = -EBADMSG; ++ if (sequence != expected++) ++ goto out; ++ if (sftl_kind(record) == 2 && record->key == SFTL_TRIM) { ++ const u8 *payload; ++ u32 count, offset; ++ ++ ret = sftl_read_address(sftl, record->address, &chunk); ++ if (ret) ++ goto out; ++ ret = -EBADMSG; ++ if (sftl_stamp(sftl->oob + chunk * SFTL_META) != record->stamp || ++ get_unaligned_le32(sftl->oob + chunk * SFTL_META + 8) != record->key) ++ goto out; ++ payload = sftl->page + chunk * SFTL_CHUNK; ++ count = get_unaligned_le32(payload); ++ if (count > (SFTL_CHUNK - 4) / 8) ++ goto out; ++ for (offset = 0; offset < count; offset++) { ++ u32 start = get_unaligned_le32(payload + 4 + offset * 8); ++ u32 length = get_unaligned_le32(payload + 8 + offset * 8); ++ ++ if (!length) ++ continue; ++ ret = sftl_replace(sftl, start, length, SFTL_UNMAPPED); ++ if (ret) ++ goto out; ++ } ++ } else { ++ ret = sftl_replace(sftl, record->key, 1, record->address); ++ if (ret) ++ goto out; ++ } ++ sftl->sequence = sequence; ++ } ++ sftl->sequence = max(sftl->sequence, floor); ++ ret = 0; ++out: ++ kvfree(journal.items); ++ return ret; ++} ++ ++int apple_sftl_load(struct apple_ppn *provider, struct apple_sftl **out) ++{ ++ struct apple_sftl *sftl; ++ u8 *heads; ++ u32 block; ++ unsigned int chunk; ++ int ret; ++ ++ if (!out) ++ return -EINVAL; ++ *out = NULL; ++ if (!provider || !provider->ops || !provider->ops->read_page || ++ provider->buses != 2 || provider->caus != 8 || ++ provider->blocks != SFTL_BLOCKS || provider->pages != 128) ++ return -EOPNOTSUPP; ++ sftl = kzalloc(sizeof(*sftl), GFP_KERNEL); ++ if (!sftl) ++ return -ENOMEM; ++ sftl->ppn = provider; ++ sftl->runs = RB_ROOT; ++ mutex_init(&sftl->lock); ++ sftl->page = kmalloc(APPLE_PPN_PAGE_SIZE, GFP_KERNEL); ++ heads = kcalloc(SFTL_BLOCKS, SFTL_META, GFP_KERNEL); ++ if (!sftl->page || !heads) { ++ ret = -ENOMEM; ++ goto failed; ++ } ++ memset(heads, 0xff, SFTL_BLOCKS * SFTL_META); ++ ret = sftl_vfl(sftl); ++ if (ret) ++ goto failed; ++ for (block = 1; block < SFTL_SUBBLOCKS; block++) { ++ if (!sftl->lanes[block]) ++ continue; ++ ret = sftl_read_address(sftl, block * SFTL_STRIDE, &chunk); ++ if (ret) ++ goto failed; ++ if (sftl_erased(sftl->oob, sizeof(sftl->oob))) { ++ if (!sftl_erased(sftl->page, APPLE_PPN_PAGE_SIZE)) { ++ ret = -EBADMSG; ++ goto failed; ++ } ++ } else if (sftl->oob[0] == 0x1f || sftl->oob[0] == 1 || sftl->oob[0] == 2) { ++ memcpy(heads + block * SFTL_META, sftl->oob, SFTL_META); ++ } else { ++ ret = -EOPNOTSUPP; ++ goto failed; ++ } ++ } ++ ret = sftl_context(sftl, heads); ++ if (ret) ++ goto failed; ++ ret = sftl_journal(sftl, heads); ++ if (ret) ++ goto failed; ++ kfree(heads); ++ *out = sftl; ++ return 0; ++failed: ++ kfree(heads); ++ apple_sftl_free(sftl); ++ return ret; ++} ++EXPORT_SYMBOL_GPL(apple_sftl_load); ++ ++void apple_sftl_free(struct apple_sftl *sftl) ++{ ++ struct rb_node *node; ++ ++ if (!sftl) ++ return; ++ while ((node = rb_first(&sftl->runs))) { ++ rb_erase(node, &sftl->runs); ++ kfree(rb_entry(node, struct sftl_run, node)); ++ } ++ kfree(sftl->page); ++ kfree(sftl); ++} ++EXPORT_SYMBOL_GPL(apple_sftl_free); ++ ++int apple_sftl_read(struct apple_sftl *sftl, u32 logical_page, u8 *data) ++{ ++ struct sftl_run *run; ++ unsigned int chunk; ++ u32 address; ++ const u8 *meta; ++ int ret = 0; ++ ++ if (!sftl || !data || logical_page >= SFTL_LPAGES) ++ return -EINVAL; ++ mutex_lock(&sftl->lock); ++ if (sftl->failed) { ++ ret = -EIO; ++ goto out; ++ } ++ run = sftl_find(sftl, logical_page); ++ if (!run) { ++ ret = -EBADMSG; ++ goto failed; ++ } ++ if (run->provider == SFTL_UNMAPPED) { ++ memset(data, 0, SFTL_CHUNK); ++ goto out; ++ } ++ address = run->provider + logical_page - run->start; ++ ret = sftl_read_address(sftl, address, &chunk); ++ if (ret) ++ goto failed; ++ meta = sftl->oob + chunk * SFTL_META; ++ if ((meta[0] != 1 && meta[0] != 2) || get_unaligned_le32(meta + 8) != logical_page) { ++ ret = -EBADMSG; ++ goto failed; ++ } ++ memcpy(data, sftl->page + chunk * SFTL_CHUNK, SFTL_CHUNK); ++ goto out; ++failed: ++ sftl->failed = true; ++out: ++ mutex_unlock(&sftl->lock); ++ return ret; ++} ++EXPORT_SYMBOL_GPL(apple_sftl_read); ++ ++void apple_sftl_get_info(struct apple_sftl *sftl, struct apple_sftl_info *info) ++{ ++ info->logical_pages = SFTL_LPAGES; ++ info->disk_pages = SFTL_DISK_PAGES; ++ info->nr_runs = sftl->nr_runs; ++ info->checkpoint_sequence = sftl->checkpoint_sequence; ++ info->sequence = sftl->sequence; ++} ++EXPORT_SYMBOL_GPL(apple_sftl_get_info); +diff --git a/drivers/mtd/apple-sftl-test.c b/drivers/mtd/apple-sftl-test.c +new file mode 100644 +index 000000000000..e451704c9ab5 +--- /dev/null ++++ b/drivers/mtd/apple-sftl-test.c +@@ -0,0 +1,436 @@ ++// SPDX-License-Identifier: GPL-2.0-only ++/* Synthetic VFL/SFTL medium. No firmware or captured device bytes. */ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "apple-sftl.h" ++ ++#define TEST_LOGICAL_PAGES 15625016U ++#define TEST_STRIDE 8192U ++#define TEST_CHECKPOINT 101ULL ++#define TEST_CLOSED_FIRST 102ULL ++#define TEST_CLOSED_CHUNKS 1020U ++#define TEST_OPEN_FIRST (TEST_CLOSED_FIRST + TEST_CLOSED_CHUNKS) ++ ++enum sftl_fault { ++ SFTL_NO_FAULT, ++ SFTL_CHECKPOINT_COPY, ++ SFTL_LANE_COPY, ++ SFTL_BOOTSTRAP_DUPLICATE, ++ SFTL_INCOMPLETE_CONTEXT, ++ SFTL_JOURNAL_GAP, ++ SFTL_JOURNAL_DUPLICATE, ++ SFTL_BAD_FOOTER, ++ SFTL_READ_FAILURE, ++ SFTL_REFRESH, ++ SFTL_TRIM, ++ SFTL_ALLOCATION_FLOOR, ++ SFTL_INACTIVE_ADDRESS, ++ SFTL_OPEN_HOLE, ++ SFTL_UNKNOWN_STATUS, ++ SFTL_TORN_FOOTER, ++ SFTL_RUNTIME_FAILURE, ++}; ++ ++struct sftl_fixture { ++ struct apple_ppn provider; ++ enum sftl_fault fault; ++ unsigned int reads; ++}; ++ ++static void sftl_test_meta(u8 *meta, u8 kind, u8 flags, u64 seq, u32 key) ++{ ++ unsigned int i; ++ ++ meta[0] = kind; ++ meta[1] = flags; ++ for (i = 0; i < 6; i++) ++ meta[2 + i] = seq >> (8 * i); ++ put_unaligned_le32(key, meta + 8); ++ put_unaligned_le32(seq >> 16, meta + 12); ++} ++ ++static void sftl_test_members(u8 *data, enum sftl_fault fault) ++{ ++ unsigned int i; ++ ++ for (i = 0; i < 8; i++) { ++ put_unaligned_le16(2087, data + 0x100 + i * 4); ++ data[0x102 + i * 4] = i; ++ data[0x103 + i * 4] = 0; ++ } ++ if (fault == SFTL_BOOTSTRAP_DUPLICATE) ++ data[0x106] = 0; ++} ++ ++static int sftl_test_context(struct sftl_fixture *f, unsigned int lane, ++ unsigned int page, u8 *data, u8 *oob, u8 *status) ++{ ++ unsigned int i; ++ ++ if (lane >= 8) ++ return 0; ++ if (!page) { ++ memset(data, 0, 16384); ++ put_unaligned_le16(16, data + 10); ++ put_unaligned_le16(2086, data + 12); ++ put_unaligned_le16(2086, data + 14); ++ sftl_test_members(data, f->fault); ++ for (i = 0; i < 16; i++) ++ put_unaligned_le16(i + 1, data + 0x200 + i * 32); ++ oob[0] = 0x20; ++ oob[1] = 0xff; ++ oob[2] = 0xff; ++ oob[3] = 0; ++ put_unaligned_le32(lane < 4 ? 0xfffffff0 : 0xfffffff1, oob + 4); ++ if (f->fault == SFTL_CHECKPOINT_COPY && lane == 1) ++ data[0] ^= 1; ++ *status = 0x40; ++ } else if (page <= 16) { ++ /* Lane tables occupy pages 1..16 in each of four replicas. */ ++ if (page <= 2) ++ for (i = 1; i <= 4; i++) ++ put_unaligned_le16(0, data + i * 2); ++ oob[0] = 0x20; ++ oob[1] = page - 1; ++ oob[2] = 0; ++ oob[3] = 0; ++ if (f->fault == SFTL_LANE_COPY && lane == 1 && page == 1) ++ put_unaligned_le16(0x8000, data + 2); ++ *status = 0x40; ++ } ++ return 0; ++} ++ ++static void sftl_test_checkpoint(struct sftl_fixture *f, u8 *data, u8 *oob) ++{ ++ u8 *ranges = data + 4096; ++ u32 address = 4 * TEST_STRIDE; ++ ++ memset(data, 0xff, 16384); ++ memset(data, 0, 4096); ++ put_unaligned_le32(1, data); ++ put_unaligned_le32(1, data + 4); ++ sftl_test_meta(oob, 0x1f, 1, 100, 0); ++ if (f->fault == SFTL_INCOMPLETE_CONTEXT) ++ return; ++ if (f->fault == SFTL_INACTIVE_ADDRESS) ++ address += 1024; ++ put_unaligned_le32(0, ranges); ++ put_unaligned_le32(0xfffffff0, ranges + 4); ++ put_unaligned_le32(address, ranges + 8); ++ put_unaligned_le32(1, ranges + 12); ++ put_unaligned_le32(0x1ffffff, ranges + 16); ++ put_unaligned_le32(TEST_LOGICAL_PAGES - 1, ranges + 20); ++ sftl_test_meta(oob + 16, 0x1f, 4, TEST_CHECKPOINT, 0); ++} ++ ++static void sftl_test_open(struct sftl_fixture *f, u8 *data, u8 *oob) ++{ ++ u64 first = TEST_OPEN_FIRST; ++ unsigned int i; ++ ++ if (f->fault == SFTL_JOURNAL_GAP) ++ first++; ++ if (f->fault == SFTL_JOURNAL_DUPLICATE) ++ first--; ++ for (i = 0; i < 4; i++) { ++ memset(data + i * 4096, 'D' + i, 4096); ++ sftl_test_meta(oob + i * 16, 1, 0, first + i, 2); ++ } ++ if (f->fault == SFTL_TRIM) { ++ memset(data, 0, 4096); ++ put_unaligned_le32(1, data); ++ put_unaligned_le32(0, data + 4); ++ put_unaligned_le32(1, data + 8); ++ sftl_test_meta(oob, 2, 0, first, 0xffff0001); ++ } ++} ++ ++static void sftl_test_closed(struct sftl_fixture *f, unsigned int position, ++ u8 *data, u8 *oob) ++{ ++ unsigned int i; ++ ++ if (position < 255) { ++ for (i = 0; i < 4; i++) { ++ u32 slot = position * 4 + i; ++ ++ memset(data + i * 4096, (slot + 1) & 0xff, 4096); ++ sftl_test_meta(oob + i * 16, 1, 0, ++ TEST_CLOSED_FIRST + slot, slot + 1); ++ } ++ } else { ++ u32 span = TEST_CLOSED_CHUNKS; ++ ++ memset(data, 0, 16384); ++ put_unaligned_le32(0, data); ++ put_unaligned_le32(0, data + 4); ++ put_unaligned_le32(1, data + 8); ++ put_unaligned_le32(TEST_CLOSED_CHUNKS, data + 12); ++ if (f->fault == SFTL_ALLOCATION_FLOOR) ++ span += 8; ++ sftl_test_meta(oob, 0x1c, 8, TEST_CLOSED_FIRST, span); ++ put_unaligned_le16(f->fault == SFTL_BAD_FOOTER ? 2 : 4, oob + 12); ++ put_unaligned_le16(0xffff, oob + 14); ++ for (i = 1; i < 4; i++) ++ memcpy(oob + i * 16, oob, 16); ++ if (f->fault == SFTL_TORN_FOOTER) ++ oob[63] ^= 1; ++ } ++} ++ ++static int sftl_test_read_page(void *priv, unsigned int bus, unsigned int cau, ++ unsigned int block, unsigned int page, ++ bool context, u8 *data, u8 *oob, u8 *status) ++{ ++ struct sftl_fixture *f = priv; ++ unsigned int lane = cau * 2 + bus; ++ unsigned int position = page * 2 + lane; ++ ++ f->reads++; ++ if (bus >= 2 || cau >= 8 || block >= 2088 || page >= (context ? 64 : 128)) ++ return -EINVAL; ++ memset(data, 0xff, 16384); ++ memset(oob, 0xff, 64); ++ *status = 0x49; ++ if (context) { ++ if (block == 2087) ++ return sftl_test_context(f, lane, page, data, oob, status); ++ return 0; ++ } ++ if (block == 3 && (f->fault == SFTL_RUNTIME_FAILURE || ++ (!position && f->fault == SFTL_READ_FAILURE))) ++ return -EIO; ++ if (lane > 1) ++ return 0; ++ if (block == 1 && !position) { ++ sftl_test_checkpoint(f, data, oob); ++ } else if (block == 2 && !position) { ++ sftl_test_open(f, data, oob); ++ } else if (block == 2 && position == 2 && f->fault == SFTL_OPEN_HOLE) { ++ unsigned int i; ++ ++ for (i = 0; i < 4; i++) { ++ memset(data + i * 4096, 'H' + i, 4096); ++ sftl_test_meta(oob + i * 16, 1, 0, TEST_OPEN_FIRST + 4 + i, 3); ++ } ++ } else if (block == 3) { ++ sftl_test_closed(f, position, data, oob); ++ } else if (block == 4 && !position) { ++ memset(data, 'C', 4096); ++ sftl_test_meta(oob, 1, 0, 10, 0); ++ } else { ++ return 0; ++ } ++ *status = block == 2 && f->fault == SFTL_REFRESH ? 0x42 : 0x40; ++ if (block == 2 && f->fault == SFTL_UNKNOWN_STATUS) ++ *status = 0x61; ++ return 0; ++} ++ ++static const struct apple_ppn_ops sftl_test_ops = { ++ .read_page = sftl_test_read_page, ++}; ++ ++static struct sftl_fixture *sftl_test_fixture(struct kunit *test, ++ enum sftl_fault fault) ++{ ++ struct sftl_fixture *f = kunit_kzalloc(test, sizeof(*f), GFP_KERNEL); ++ ++ if (!f) ++ return NULL; ++ f->fault = fault; ++ f->provider.ops = &sftl_test_ops; ++ f->provider.priv = f; ++ f->provider.buses = 2; ++ f->provider.caus = 8; ++ f->provider.blocks = 2088; ++ f->provider.pages = 128; ++ return f; ++} ++ ++static void sftl_test_expect_fill(struct kunit *test, const u8 *data, u8 fill) ++{ ++ unsigned int i; ++ ++ for (i = 0; i < 4096; i++) ++ if (data[i] != fill) { ++ KUNIT_FAIL(test, "logical byte %u: expected %02x, got %02x", ++ i, fill, data[i]); ++ return; ++ } ++} ++ ++static void sftl_test_complete_medium(struct kunit *test) ++{ ++ struct sftl_fixture *f = sftl_test_fixture(test, SFTL_NO_FAULT); ++ struct apple_sftl_info info; ++ struct apple_sftl *sftl = NULL; ++ u8 *data = kunit_kmalloc(test, 4096, GFP_KERNEL); ++ ++ KUNIT_ASSERT_NOT_NULL(test, f); ++ KUNIT_ASSERT_NOT_NULL(test, data); ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ KUNIT_ASSERT_NOT_NULL(test, sftl); ++ apple_sftl_get_info(sftl, &info); ++ KUNIT_EXPECT_EQ(test, info.logical_pages, TEST_LOGICAL_PAGES); ++ KUNIT_EXPECT_EQ(test, info.checkpoint_sequence, TEST_CHECKPOINT); ++ KUNIT_EXPECT_EQ(test, info.sequence, TEST_OPEN_FIRST + 3); ++ KUNIT_EXPECT_EQ(test, info.nr_runs, 5U); ++ KUNIT_EXPECT_EQ(test, info.disk_pages, 15625000U); ++ KUNIT_EXPECT_LT(test, f->reads, 12000U); ++ KUNIT_EXPECT_EQ(test, apple_sftl_read(sftl, 0, data), 0); ++ sftl_test_expect_fill(test, data, 'C'); ++ KUNIT_EXPECT_EQ(test, apple_sftl_read(sftl, 2, data), 0); ++ sftl_test_expect_fill(test, data, 'G'); ++ KUNIT_EXPECT_EQ(test, apple_sftl_read(sftl, 6, data), 0); ++ sftl_test_expect_fill(test, data, 6); ++ KUNIT_EXPECT_EQ(test, apple_sftl_read(sftl, 2000, data), 0); ++ sftl_test_expect_fill(test, data, 0); ++ KUNIT_EXPECT_LT(test, apple_sftl_read(sftl, TEST_LOGICAL_PAGES, data), 0); ++ apple_sftl_free(sftl); ++} ++ ++static void sftl_test_reject_incomplete_or_ambiguous(struct kunit *test) ++{ ++ static const enum sftl_fault faults[] = { ++ SFTL_CHECKPOINT_COPY, SFTL_LANE_COPY, SFTL_BOOTSTRAP_DUPLICATE, ++ SFTL_INCOMPLETE_CONTEXT, SFTL_JOURNAL_GAP, ++ SFTL_JOURNAL_DUPLICATE, SFTL_BAD_FOOTER, ++ SFTL_READ_FAILURE, SFTL_REFRESH, SFTL_UNKNOWN_STATUS, ++ SFTL_TORN_FOOTER, ++ }; ++ unsigned int i; ++ ++ for (i = 0; i < ARRAY_SIZE(faults); i++) { ++ struct sftl_fixture *f = sftl_test_fixture(test, faults[i]); ++ struct apple_sftl *sftl = NULL; ++ ++ KUNIT_ASSERT_NOT_NULL(test, f); ++ KUNIT_EXPECT_LT_MSG(test, apple_sftl_load(&f->provider, &sftl), 0, ++ "fault %u", faults[i]); ++ KUNIT_EXPECT_PTR_EQ(test, sftl, NULL); ++ apple_sftl_free(sftl); ++ } ++} ++ ++static void sftl_test_trim_then_overwrite(struct kunit *test) ++{ ++ struct sftl_fixture *f = sftl_test_fixture(test, SFTL_TRIM); ++ struct apple_sftl *sftl = NULL; ++ u8 *data = kunit_kmalloc(test, 4096, GFP_KERNEL); ++ ++ KUNIT_ASSERT_NOT_NULL(test, f); ++ KUNIT_ASSERT_NOT_NULL(test, data); ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ KUNIT_EXPECT_EQ(test, apple_sftl_read(sftl, 0, data), 0); ++ sftl_test_expect_fill(test, data, 0); ++ KUNIT_EXPECT_EQ(test, apple_sftl_read(sftl, 2, data), 0); ++ sftl_test_expect_fill(test, data, 'G'); ++ apple_sftl_free(sftl); ++} ++ ++static void sftl_test_preserve_footer_allocation_floor(struct kunit *test) ++{ ++ struct sftl_fixture *f = sftl_test_fixture(test, SFTL_ALLOCATION_FLOOR); ++ struct apple_sftl_info info; ++ struct apple_sftl *sftl = NULL; ++ ++ KUNIT_ASSERT_NOT_NULL(test, f); ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ apple_sftl_get_info(sftl, &info); ++ KUNIT_EXPECT_EQ(test, info.sequence, ++ TEST_CLOSED_FIRST + TEST_CLOSED_CHUNKS + 8 - 1); ++ apple_sftl_free(sftl); ++} ++ ++static void sftl_test_reject_other_geometry(struct kunit *test) ++{ ++ struct sftl_fixture *f = sftl_test_fixture(test, SFTL_NO_FAULT); ++ struct apple_sftl *sftl = NULL; ++ ++ KUNIT_ASSERT_NOT_NULL(test, f); ++ f->provider.caus = 2; ++ KUNIT_EXPECT_LT(test, apple_sftl_load(&f->provider, &sftl), 0); ++ KUNIT_EXPECT_PTR_EQ(test, sftl, NULL); ++ KUNIT_EXPECT_EQ(test, f->reads, 0U); ++} ++ ++static void sftl_test_reject_inactive_lane_gap(struct kunit *test) ++{ ++ struct sftl_fixture *f = sftl_test_fixture(test, SFTL_INACTIVE_ADDRESS); ++ struct apple_sftl *sftl = NULL; ++ u8 *data = kunit_kmalloc(test, 4096, GFP_KERNEL); ++ int ret; ++ ++ KUNIT_ASSERT_NOT_NULL(test, f); ++ KUNIT_ASSERT_NOT_NULL(test, data); ++ ret = apple_sftl_load(&f->provider, &sftl); ++ if (!ret) { ++ KUNIT_EXPECT_LT(test, apple_sftl_read(sftl, 0, data), 0); ++ apple_sftl_free(sftl); ++ } else { ++ KUNIT_EXPECT_PTR_EQ(test, sftl, NULL); ++ } ++} ++ ++static void sftl_test_scan_after_open_lane_hole(struct kunit *test) ++{ ++ struct sftl_fixture *f = sftl_test_fixture(test, SFTL_OPEN_HOLE); ++ struct apple_sftl *sftl = NULL; ++ struct apple_sftl_info info; ++ u8 *data = kunit_kmalloc(test, 4096, GFP_KERNEL); ++ ++ KUNIT_ASSERT_NOT_NULL(test, f); ++ KUNIT_ASSERT_NOT_NULL(test, data); ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ apple_sftl_get_info(sftl, &info); ++ KUNIT_EXPECT_EQ(test, info.sequence, TEST_OPEN_FIRST + 7); ++ KUNIT_EXPECT_EQ(test, apple_sftl_read(sftl, 3, data), 0); ++ sftl_test_expect_fill(test, data, 'K'); ++ apple_sftl_free(sftl); ++} ++ ++static void sftl_test_runtime_failure_stops_reads(struct kunit *test) ++{ ++ struct sftl_fixture *f = sftl_test_fixture(test, SFTL_NO_FAULT); ++ struct apple_sftl *sftl = NULL; ++ u8 *data = kunit_kmalloc(test, 4096, GFP_KERNEL); ++ unsigned int reads; ++ ++ KUNIT_ASSERT_NOT_NULL(test, f); ++ KUNIT_ASSERT_NOT_NULL(test, data); ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ f->fault = SFTL_RUNTIME_FAILURE; ++ KUNIT_EXPECT_LT(test, apple_sftl_read(sftl, 6, data), 0); ++ reads = f->reads; ++ KUNIT_EXPECT_LT(test, apple_sftl_read(sftl, 0, data), 0); ++ KUNIT_EXPECT_EQ(test, f->reads, reads); ++ apple_sftl_free(sftl); ++} ++ ++static struct kunit_case apple_sftl_cases[] = { ++ KUNIT_CASE(sftl_test_complete_medium), ++ KUNIT_CASE(sftl_test_scan_after_open_lane_hole), ++ KUNIT_CASE(sftl_test_runtime_failure_stops_reads), ++ KUNIT_CASE(sftl_test_reject_incomplete_or_ambiguous), ++ KUNIT_CASE(sftl_test_trim_then_overwrite), ++ KUNIT_CASE(sftl_test_preserve_footer_allocation_floor), ++ KUNIT_CASE(sftl_test_reject_other_geometry), ++ KUNIT_CASE(sftl_test_reject_inactive_lane_gap), ++ {} ++}; ++ ++static struct kunit_suite apple_sftl_suite = { ++ .name = "apple-sftl", ++ .test_cases = apple_sftl_cases, ++}; ++ ++kunit_test_suite(apple_sftl_suite); ++ ++MODULE_LICENSE("GPL"); +diff --git a/drivers/mtd/apple-sftl.h b/drivers/mtd/apple-sftl.h +new file mode 100644 +index 000000000000..7a05b95d1109 +--- /dev/null ++++ b/drivers/mtd/apple-sftl.h +@@ -0,0 +1,23 @@ ++/* SPDX-License-Identifier: GPL-2.0-only */ ++#ifndef _APPLE_SFTL_H ++#define _APPLE_SFTL_H ++ ++#include ++#include ++ ++struct apple_sftl; ++ ++struct apple_sftl_info { ++ u32 logical_pages; ++ u32 disk_pages; ++ u32 nr_runs; ++ u64 checkpoint_sequence; ++ u64 sequence; ++}; ++ ++int apple_sftl_load(struct apple_ppn *provider, struct apple_sftl **out); ++void apple_sftl_free(struct apple_sftl *sftl); ++int apple_sftl_read(struct apple_sftl *sftl, u32 logical_page, u8 *data); ++void apple_sftl_get_info(struct apple_sftl *sftl, struct apple_sftl_info *info); ++ ++#endif +diff --git a/tools/testing/kunit/configs/apple_sftl.config b/tools/testing/kunit/configs/apple_sftl.config +new file mode 100644 +index 000000000000..5ee7e49bbed1 +--- /dev/null ++++ b/tools/testing/kunit/configs/apple_sftl.config +@@ -0,0 +1,4 @@ ++CONFIG_KUNIT=y ++CONFIG_MTD=y ++CONFIG_MTD_APPLE_SFTL=y ++CONFIG_MTD_APPLE_SFTL_KUNIT_TEST=y From e3fb573c0f3989380a98beffd23bd0eff85187e4 Mon Sep 17 00:00:00 2001 From: Shayne Legault <8741557+shaynelegault@users.noreply.github.com> Date: Fri, 6 Nov 2026 12:59:53 -0600 Subject: [PATCH 3/4] p105: record experimental native append evidence and limits Describe the kernel append, summary replay and logical write policy following native-map recovery. Keep storage_write missing and retain the installation gate while the exact kernel revision awaits physical write qualification. Record separate A5 append evidence and the outstanding recovery, persistence and reclamation work. The source passes both current ARMv7 object variants and 26 real Linux KUnit cases. Host checks cover request updates and bounded provider forwarding. Document their scope together with default-off policy and NOIO request allocation. Kernel patch export remains a separate step. Regenerate the support matrix and run all 34 catalog tests with the checkout's absolute PYTHONPATH; both storage states stay missing. Assisted-by: LLM --- docs/bring-up/p105-sftl-append.md | 89 +++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 docs/bring-up/p105-sftl-append.md diff --git a/docs/bring-up/p105-sftl-append.md b/docs/bring-up/p105-sftl-append.md new file mode 100644 index 00000000..d209139b --- /dev/null +++ b/docs/bring-up/p105-sftl-append.md @@ -0,0 +1,89 @@ +# P105 native append writes + +The kernel `apple-sftl` writer appends to Apple's shared VFL/SwissPPNFTL +medium on the measured 64 GB P105. Native records carry the updated logical +map across reload. The boot reader must follow VFL and SFTL mapping to find +Linux data. LwVM partitions sit above the recovered `applesftlN` disk. +The [reader note](p105-sftl-reader.md) describes geometry and context recovery. + +## Source and format + +Kernel branch `p105/07-ftl-append` ends at +`c63274165870417f01cc3e1264067cef71d9c0b2`. It contains the verified PPN +program callback (`ab137f3be`), append/replay path (`cb20cc76a`) and request +allocation guard (`c63274165`), following the read-only reader and program +primitive dependencies. Kernel patch export remains pending integration. + +The allocator reserves the boot and package tails, every saved SFTL context, +validated VFL bootstrap members and blocks referenced by the recovered map. +It checks every data and metadata byte across a candidate's active lanes +before programming. An occupied candidate is skipped. A failed storage transaction stops the +instance; exhausted blank space returns `ENOSPC` while reads +remain available. The allocator performs no erase or reclamation. + +A program writes a complete 16 KiB page with 64 canonical metadata bytes. +One through four logical 4 KiB entries occupy its chunks; short requests +repeat the last entry with a distinct sequence for each copy. The provider +waits for completion and verifies exact readback before map publication. +Filled blocks receive eight summary pages containing native type-1c runs. +The span field at metadata offset eight is the next sequence minus the block +base; it reserves no additional eight sequence slots. + +A fresh loader replays open data records or a completed summary. An +interrupted summary can be ignored only after its entire data prefix +validates. Torn or ambiguous metadata stops recovery. Recovery retains its +contiguous journal rule, so an allocation floor beyond the final visible +record prevents writes before mutation. The saved measured map has equal +allocation and journal sequence 187831212. + +The frontend preserves untouched sectors during partial 4 KiB updates and +groups up to four logical pages per physical program. Flush and FUA complete +after synchronous readback. Request processing uses a NOIO allocation scope, +which prevents map or provider allocations from recursing into writeback +while the request lock is held. + +## Explicit operating policy + +Both `MTD_APPLE_H2FMI_WRITE` and `MTD_APPLE_SFTL_WRITE` default off. Logical +writes also require `write_enable=1` when loading `apple-sftl`, or +`apple_sftl.write_enable=1` for a built-in driver. The parameter defaults to +false and is read-only after load. A supported replay state and a program +callback are required. The installer must fence its intended logical range; +this policy controls the logical disk independently of raw MTD permissions. + +`max_page_reads` defaults to 32768 explicit canonical read callbacks across +recovery, candidate scans and runtime reads. Program-internal blank checks +and readback remain inside the program callback and outside that counter. +`max_io_time_ms` defaults to 600000 milliseconds from probe. Programs check +the deadline before and after the synchronous callback. Both limit parameters +are read-only and accept 1 through 4294967295. Exhaustion or a provider error +permanently stops the instance. These defaults bound qualification; an +installed root needs an explicit operating policy and must stay within it. + +## Evidence and qualification + +The exact source passed these software checks: + +- Current ARMv7 core, block, fixture and H2FMI objects compiled in read-only + and write-enabled variants with `W=1` and warnings as errors. The existing + `vmstat.h` enum-arithmetic warning remained a warning. Built-in parameter + prefixes were verified from the objects. +- All 26 tests passed in a Linux KUnit kernel using pinned pristine v6.12, + exact core/block/test sources and a validation-only Kconfig dependency + symbol for the mock program provider. This run excludes H2FMI hardware. + Cases cover all 16 lanes, 8160 separate footer entries and fresh replay. +- Host ASan/UBSan checks passed 26 core cases and 13 read-only cases with + allocation, mutex and tree-API shims. Extracted request helpers passed 25 + cases plus 2048 varied requests. Queue/policy and provider-wrapper checks + cover deadline failures and balanced allocation scope. +- The actual C core reconstructed saved private pages and matched 28830 map + boundary checks against the Python reference, plus native LwVM and System + prefixes. This issued zero device operations. Public fixtures generate + synthetic bytes. + +The exact kernel writer has no physical-device write qualification. The +earlier unplugged Omarchy desktop used the local Python backend. A fresh +approved boot must establish recovery access and bounded reads before any +approved write work. Native iOS recovery after writes, reboot persistence +and interruption handling need physical evidence. P105 `storage_write` +remains `missing`, with installation gated by the current feature states. From 45300e435d13f5161691ae5324295fe8815d0fda Mon Sep 17 00:00:00 2001 From: Henrik Klee Date: Sat, 7 Nov 2026 20:31:14 +0800 Subject: [PATCH 4/4] kernel: apple-armv7 iOS disk append writes Add the append writes of #91 to kernel/patches/armv7/mtd: the iOS disk of the 64 GB iPad mini 1 becomes writable. Writing is part of the driver; the default-off MTD_APPLE_SFTL_WRITE option and the write_enable module parameter are gone. The disk stays read-only only when the replayed journal does not end at the allocation floor. Without reclamation (#93), writes fail once the blank blocks are used up. The patch keeps Shayne Legault as author and notes the changes and his original commit IDs. The bring-up notes and the board README follow the driver as carried. Assisted-by: LLM --- devices/p105/README.md | 12 +- docs/bring-up/p105-sftl-append.md | 24 +- ...005-mtd-apple-sftl-Add-append-writes.patch | 1563 +++++++++++++++++ 3 files changed, 1577 insertions(+), 22 deletions(-) create mode 100644 kernel/patches/armv7/mtd/0005-mtd-apple-sftl-Add-append-writes.patch diff --git a/devices/p105/README.md b/devices/p105/README.md index 407c2177..cde705f6 100644 --- a/devices/p105/README.md +++ b/devices/p105/README.md @@ -29,11 +29,11 @@ public recipe or release. ## Open work -- Storage: the NAND driver (OrchardLinux/orchard#86, #87) and the iOS disk - translation layer (#89) are in `kernel/patches/armv7/mtd`; the disk is - `applesftlN`, on the 64 GB model only, read-only. Its append writes (#91) - follow on top. Reclamation (#93) is unfinished: until it lands, writes - stop once the blank flash blocks are used up. +- Storage: the NAND driver (OrchardLinux/orchard#86, #87), the iOS disk + translation layer and its append writes (#89, #91) are in + `kernel/patches/armv7/mtd`; the disk is `applesftlN`, on the 64 GB model + only. Reclamation (#93) is unfinished: until it lands, writes stop once + the blank flash blocks are used up. - A Linux root on LwVM: the kernel reads LwVM partitions (`kernel/patches/armv7/block`), but the shared initrd finds its root by reading a GPT itself and needs to learn LwVM partitions. @@ -51,3 +51,5 @@ public recipe or release. yet; it waits until 32-bit boots bind their own configfs gadget. - #89's read budget and lifetime module parameters (ffa74e8, source e7d118292) and its module build (a607325): dropped; the reader is built in. +- #91's `MTD_APPLE_SFTL_WRITE` option and `write_enable` parameter (e049bb4): + removed, writing is part of the driver. diff --git a/docs/bring-up/p105-sftl-append.md b/docs/bring-up/p105-sftl-append.md index d209139b..146b88f6 100644 --- a/docs/bring-up/p105-sftl-append.md +++ b/docs/bring-up/p105-sftl-append.md @@ -42,23 +42,13 @@ after synchronous readback. Request processing uses a NOIO allocation scope, which prevents map or provider allocations from recursing into writeback while the request lock is held. -## Explicit operating policy +## Operating policy -Both `MTD_APPLE_H2FMI_WRITE` and `MTD_APPLE_SFTL_WRITE` default off. Logical -writes also require `write_enable=1` when loading `apple-sftl`, or -`apple_sftl.write_enable=1` for a built-in driver. The parameter defaults to -false and is read-only after load. A supported replay state and a program -callback are required. The installer must fence its intended logical range; -this policy controls the logical disk independently of raw MTD permissions. - -`max_page_reads` defaults to 32768 explicit canonical read callbacks across -recovery, candidate scans and runtime reads. Program-internal blank checks -and readback remain inside the program callback and outside that counter. -`max_io_time_ms` defaults to 600000 milliseconds from probe. Programs check -the deadline before and after the synchronous callback. Both limit parameters -are read-only and accept 1 through 4294967295. Exhaustion or a provider error -permanently stops the instance. These defaults bound qualification; an -installed root needs an explicit operating policy and must stay within it. +Orchard carries the writer in `kernel/patches/armv7/mtd` with writing as part +of the driver: there is no build option, module parameter or read budget. The +disk is writable when the provider can program pages and the replayed journal +ends at the allocation floor; otherwise it is read-only. Raw MTD devices stay +available beside it. ## Evidence and qualification @@ -86,4 +76,4 @@ earlier unplugged Omarchy desktop used the local Python backend. A fresh approved boot must establish recovery access and bounded reads before any approved write work. Native iOS recovery after writes, reboot persistence and interruption handling need physical evidence. P105 `storage_write` -remains `missing`, with installation gated by the current feature states. +is `untested` until a device test. diff --git a/kernel/patches/armv7/mtd/0005-mtd-apple-sftl-Add-append-writes.patch b/kernel/patches/armv7/mtd/0005-mtd-apple-sftl-Add-append-writes.patch new file mode 100644 index 00000000..96b65471 --- /dev/null +++ b/kernel/patches/armv7/mtd/0005-mtd-apple-sftl-Add-append-writes.patch @@ -0,0 +1,1563 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Shayne Legault <8741557+shaynelegault@users.noreply.github.com> +Date: Fri, 6 Nov 2026 12:34:23 -0600 +Subject: [PATCH] mtd: apple-sftl: Add append writes + +Append complete pages to blank VFL blocks while leaving Apple's map, saved +contexts and VFL bootstrap blocks in place. Data records carry native +metadata, and a full block gets native summary pages. Before a block is +used, every page on every lane is checked to be blank. A mapping is only +published after the provider has programmed and verified the page, and +flush and FUA complete only verified writes. Fresh recovery replays the +appended records, including a complete data prefix whose summary was +interrupted. + +Partial sector writes are read-modify-write; up to four 4 KiB logical pages +go into one physical program. Request processing runs in a NOIO allocation +scope, so reclaim cannot recurse into the disk while its lock is held. + +There is no erase or garbage collection yet: writes fail with ENOSPC once +the blank blocks are used up, and reads keep working. The disk is read-only +when the replayed journal does not end at the allocation floor. + +[Orchard: from shaynelegault/orchard#91 e049bb4 (sources cb20cc76a and + c63274165); removed the default-off MTD_APPLE_SFTL_WRITE option and the + write_enable module parameter, so writing is part of the driver; folded + the NOIO follow-up into this patch; shortened the commit message] + +Assisted-by: LLM +--- + Documentation/driver-api/mtd/apple-sftl.rst | 83 ++- + drivers/mtd/Kconfig | 11 +- + drivers/mtd/apple-sftl-block.c | 105 +++- + drivers/mtd/apple-sftl-core.c | 310 ++++++++++- + drivers/mtd/apple-sftl-test.c | 589 +++++++++++++++++++- + drivers/mtd/apple-sftl.h | 4 + + 6 files changed, 1053 insertions(+), 49 deletions(-) + +diff --git a/Documentation/driver-api/mtd/apple-sftl.rst b/Documentation/driver-api/mtd/apple-sftl.rst +index 8295a5a2a84b..d683385bdcc4 100644 +--- a/Documentation/driver-api/mtd/apple-sftl.rst ++++ b/Documentation/driver-api/mtd/apple-sftl.rst +@@ -1,7 +1,7 @@ + .. SPDX-License-Identifier: GPL-2.0-only + +-Apple PPN VFL/SwissPPNFTL reader +-============================== ++Apple PPN VFL/SwissPPNFTL ++======================== + + The ``apple-sftl`` driver recovers the logical medium above Apple's PPN flash + mapping. H2FMI supplies complete 16 KiB pages and their 64 metadata bytes +@@ -23,39 +23,84 @@ which the reader rejects. + SFTL subtype-4 context fragments describe the saved logical ranges. Recovery + validates the complete context sequence, then applies later data and trim + records in sequence order. Completed blocks carry summaries at their ends. +-Open blocks are scanned completely because an interrupted stripe may leave +-an erased lane before another completed lane. Summary allocation spans also ++Open blocks are scanned completely because an interrupted stripe may leave an ++erased lane before another completed lane. Summary allocation spans also + preserve the next sequence floor when it exceeds the last visible record. + + The map has 15,625,016 entries, including sixteen internal keys. The block +-device exposes 15,625,000 entries, or 64,000,000,000 bytes, with 512-byte sector +-reads. Linux's disk name is ``applesftlN``. LwVM and filesystem interpretation +-happen above this logical provider. Encrypted partition contents remain +-ciphertext. ++device exposes 15,625,000 entries, or 64,000,000,000 bytes, with 512-byte ++sector reads and optional read-modify-write sector updates. Linux's disk name ++is ``applesftlN``. LwVM and filesystem interpretation happen above this ++logical provider. Encrypted partition contents remain ciphertext. + + Failure and lifetime rules + -------------------------- + +-The disk is read-only and the provider has no programming or erase operation. + Malformed context copies and incomplete journal sequences prevent disk +-registration. A refresh status requires a writer and is reported as an error. +-A failed runtime read stops subsequent reads from this map instance. ++registration. A refresh status is reported as an error. A failed runtime read stops subsequent I/O from this map instance. + + H2FMI unregisters the child before releasing its controller state. The block + frontend freezes and drains requests during removal. Its request buffer is + allocated before the disk becomes visible, while blocking requests serialize +-access to that buffer and the provider. ++access to that buffer and the provider. Request processing uses a NOIO ++allocation scope so map and provider buffers cannot trigger recursive ++writeback while the queue lock is held. ++ ++Append writes ++------------- ++ ++The aggregate provider must accept a complete 16 ++KiB page with its 64 metadata bytes, wait for completion, and verify exact ++readback before reporting success. The FTL publishes the new mapping after ++that success. A short logical write repeats its final 4 KiB entry to fill the ++physical program unit; each copy carries its own sequence. ++ ++The allocator reserves every saved SFTL context block, all validated VFL ++bootstrap member blocks, every block referenced by the recovered map, and the ++boot and package tail blocks. It selects a block whose head is erased, then ++checks every physical page and metadata byte on every active lane before ++programming any page. An occupied candidate is skipped permanently within the ++instance. A failed scan stops I/O. Allocation scans have a 500-second limit; ++the block queue allows 600 seconds for the scan and synchronous programs. ++ ++Data records use native type-1 metadata. Once a block's data region is full, ++the driver writes eight summary pages containing native type-1c run entries. ++The old Apple checkpoint and the shared map remain intact. Fresh recovery ++replays completed data records from an open block, or uses a complete summary. ++An interrupted summary can be ignored only when its metadata identifies the ++reserved summary tail and the entire preceding data prefix validates in ++physical and sequence order. Torn or ambiguous metadata stops recovery. ++ ++The recovered allocation floor can exceed the last visible journal record. ++This implementation retains its contiguous journal replay rule, so a mismatch ++keeps the disk read-only and returns ``-EOPNOTSUPP`` before any program. ++Native forward-gap handling requires a separate recovery change with format- ++specific fixtures. A program or map-publication error stops subsequent I/O; a ++fresh loader must determine which complete records reached the medium. ++ ++The block frontend preserves untouched sectors in partial 4 KiB updates and ++groups up to four logical pages per program. Every acknowledged write has ++completed readback. Flush and FUA use that same completion rule. There is no ++erase, discard, checkpoint update, or garbage collection. Allocation returns ++``-ENOSPC`` when verified blank blocks run out, and reads remain available. ++Writes are only enabled when the provider can program pages and the replayed ++journal ends at the allocation floor. + + Tests + ----- + + ``tools/testing/kunit/configs/apple_sftl.config`` enables the synthetic medium +-tests. They generate VFL replicas and lane tables, plus an SFTL checkpoint +-followed by completed and open data blocks. Tests cover trim ordering and +-allocation floors. Corrupt replicas and sequence gaps must return an error +-with no disk or usable map. Test inputs contain no captured device bytes. ++tests; a synthetic provider stands in for the controller. The fixtures ++generate VFL replicas and lane tables, plus an SFTL checkpoint followed by ++completed and open data blocks. Tests cover trim ordering and allocation floors. They also program synthetic pages and load a ++fresh map after open and closed blocks, interrupted summaries, and uncertain ++programs. Fixtures also cover complete candidate scans, VFL member ++reservations, space exhaustion, sequence-floor refusal and the final 48-bit ++sequence. Corrupt replicas and sequence gaps must return an error with no disk ++or usable map. Test inputs contain no captured device bytes. + + The format rules were ported from Orchard's P105 Python reference +-(``ftl_map.py``, ``ftl_load.py`` and ``ftl_log.py``), which was checked against +-native iOS logical reads. A kernel object compile or synthetic test result +-does not establish physical-controller timing or qualify a new device layout. ++(``ftl_map.py``, ``ftl_load.py``, ``ftl_log.py`` and ``ftl_append.py``), which ++was checked against native iOS logical reads. A kernel object compile or ++synthetic test result does not establish physical-controller timing or qualify ++a new device layout. +diff --git a/drivers/mtd/Kconfig b/drivers/mtd/Kconfig +index 155136a36a91..7771393e2862 100644 +--- a/drivers/mtd/Kconfig ++++ b/drivers/mtd/Kconfig +@@ -207,7 +207,7 @@ config MTD_PARTITIONED_MASTER + what lies behind the master. + + config MTD_APPLE_SFTL +- tristate "Apple PPN read-only VFL/SwissPPNFTL translation" ++ tristate "Apple PPN VFL/SwissPPNFTL translation" + depends on BLOCK + help + Recover the logical block medium stored in Apple VFL/SwissPPNFTL +@@ -216,16 +216,21 @@ config MTD_APPLE_SFTL + Unsupported checkpoints, missing journal sequences and refresh + requirements stop recovery before a disk is registered. + +- The disk is read-only. This driver performs no program or erase. ++ Writes are appended to verified blank blocks. There is no erase or ++ garbage collection yet, so writes fail with ENOSPC once the blank ++ blocks are used up; reads keep working. + + config MTD_APPLE_SFTL_KUNIT_TEST +- tristate "KUnit tests for Apple VFL/SwissPPNFTL recovery" if !KUNIT_ALL_TESTS ++ tristate "KUnit tests for Apple VFL/SwissPPNFTL" if !KUNIT_ALL_TESTS + depends on KUNIT && MTD_APPLE_SFTL + default KUNIT_ALL_TESTS + help + Synthesize complete VFL/SFTL media and exercise checkpoint recovery, + native block summaries and open-block journal replay. Malformed + replicas and unsupported layouts must fail without storage writes. ++ Also test complete programs, interrupted summaries, allocation ++ limits and fresh-loader replay. ++ All test media and provider operations are synthetic. + + source "drivers/mtd/chips/Kconfig" + +diff --git a/drivers/mtd/apple-sftl-block.c b/drivers/mtd/apple-sftl-block.c +index 86d02921b153..f1998b3242ce 100644 +--- a/drivers/mtd/apple-sftl-block.c ++++ b/drivers/mtd/apple-sftl-block.c +@@ -1,12 +1,14 @@ + // SPDX-License-Identifier: GPL-2.0-only +-/* Read-only block frontend for Apple's VFL/SwissPPNFTL logical medium. */ ++/* Block frontend for Apple's VFL/SwissPPNFTL logical medium. */ + #include + #include + #include + #include ++#include + #include + #include + #include ++#include + #include + + #include "apple-sftl.h" +@@ -23,6 +25,7 @@ struct sftl_disk { + u8 *buffer; + unsigned int id; + bool dead; ++ bool writable; + }; + + static DEFINE_IDA(sftl_disk_ids); +@@ -64,29 +67,98 @@ static int sftl_read_request(struct sftl_disk *sftl, struct request *request) + return 0; + } + ++/* Preserve untouched sectors at both ends and collect up to four 4 KiB ++ * pages per physical program. Request segments can split a logical page. ++ */ ++static int sftl_write_request(struct sftl_disk *sftl, struct request *request) ++{ ++ struct req_iterator iterator; ++ struct bio_vec bvec; ++ u64 position = (u64)blk_rq_pos(request) << SECTOR_SHIFT; ++ u32 cached = U32_MAX, first = 0; ++ unsigned int count = 0; ++ int ret; ++ ++ if (request->cmd_flags & REQ_PREFLUSH) { ++ ret = apple_sftl_flush(sftl->ftl); ++ if (ret) ++ return ret; ++ } ++ rq_for_each_segment(bvec, request, iterator) { ++ unsigned int done = 0; ++ ++ while (done < bvec.bv_len) { ++ u32 logical = position >> 12; ++ unsigned int offset = position & (SFTL_LOGICAL_BYTES - 1); ++ unsigned int length = min(bvec.bv_len - done, ++ SFTL_LOGICAL_BYTES - offset); ++ void *mapped; ++ ++ if (logical != cached) { ++ if (count == 4) { ++ ret = apple_sftl_write(sftl->ftl, first, ++ sftl->buffer, count); ++ if (ret) ++ return ret; ++ count = 0; ++ } ++ if (!count) ++ first = logical; ++ ret = apple_sftl_read(sftl->ftl, logical, ++ sftl->buffer + count * SFTL_LOGICAL_BYTES); ++ if (ret) ++ return ret; ++ count++; ++ cached = logical; ++ } ++ mapped = kmap_local_page(bvec.bv_page); ++ memcpy(sftl->buffer + (count - 1) * SFTL_LOGICAL_BYTES + offset, ++ mapped + bvec.bv_offset + done, length); ++ kunmap_local(mapped); ++ done += length; ++ position += length; ++ } ++ } ++ if (count) { ++ ret = apple_sftl_write(sftl->ftl, first, sftl->buffer, count); ++ if (ret) ++ return ret; ++ } ++ /* Every completed write is already verified on media. FUA shares that ++ * completion rule and never acknowledges a volatile cached page. ++ */ ++ return request->cmd_flags & REQ_FUA ? apple_sftl_flush(sftl->ftl) : 0; ++} ++ + static blk_status_t sftl_queue_rq(struct blk_mq_hw_ctx *hctx, + const struct blk_mq_queue_data *data) + { + struct request *request = data->rq; + struct sftl_disk *sftl = request->q->queuedata; + sector_t capacity = get_capacity(sftl->disk); ++ unsigned int noio_flags; + blk_status_t status; + int ret = 0; + +- blk_mq_start_request(request); + mutex_lock(&sftl->io_lock); ++ /* Map and provider buffers may allocate while the root disk is busy. */ ++ noio_flags = memalloc_noio_save(); ++ blk_mq_start_request(request); + if (sftl->dead) { + ret = -ENODEV; + } else if (req_op(request) == REQ_OP_FLUSH) { +- /* A read-only device has no buffered writes to flush. */ +- } else if (req_op(request) != REQ_OP_READ) { ++ ret = apple_sftl_flush(sftl->ftl); ++ } else if (req_op(request) != REQ_OP_READ && req_op(request) != REQ_OP_WRITE) { + ret = -EOPNOTSUPP; + } else if (blk_rq_pos(request) > capacity || + blk_rq_sectors(request) > capacity - blk_rq_pos(request)) { + ret = -EIO; ++ } else if (req_op(request) == REQ_OP_WRITE) { ++ ret = sftl->writable ? sftl_write_request(sftl, request) : -EROFS; + } else { + ret = sftl_read_request(sftl, request); + } ++ memalloc_noio_restore(noio_flags); + mutex_unlock(&sftl->io_lock); + status = errno_to_blk_status(ret); + blk_mq_end_request(request, status); +@@ -114,13 +186,13 @@ static int sftl_disk_probe(struct platform_device *pdev) + struct gendisk *disk; + int ret, id; + +- if (!provider) ++ if (!provider || !provider->ops || !provider->ops->read_page) + return -EINVAL; + sftl = kzalloc(sizeof(*sftl), GFP_KERNEL); + if (!sftl) + return -ENOMEM; + mutex_init(&sftl->io_lock); +- sftl->buffer = kmalloc(SFTL_LOGICAL_BYTES, GFP_KERNEL); ++ sftl->buffer = kmalloc(APPLE_PPN_PAGE_SIZE, GFP_KERNEL); + if (!sftl->buffer) { + ret = -ENOMEM; + goto free_state; +@@ -133,6 +205,13 @@ static int sftl_disk_probe(struct platform_device *pdev) + ret = -EINVAL; + goto free_ftl; + } ++ /* Appending continues the journal; it must end where the map does. */ ++ sftl->writable = provider->ops->program_page && ++ info.sequence == info.journal_sequence; ++ if (provider->ops->program_page && !sftl->writable) ++ dev_warn(&pdev->dev, "journal ends before the allocation floor, read-only\n"); ++ if (sftl->writable) ++ limits.features |= BLK_FEAT_FUA; + id = ida_alloc_range(&sftl_disk_ids, 0, MINORMASK / SFTL_DISK_MINORS, + GFP_KERNEL); + if (id < 0) { +@@ -144,6 +223,11 @@ static int sftl_disk_probe(struct platform_device *pdev) + BLK_MQ_F_SHOULD_MERGE | BLK_MQ_F_BLOCKING); + if (ret) + goto free_id; ++ /* First use verifies every page in a candidate block. Leave time for ++ * the bounded 500-second scan and synchronous data/footer programs. ++ */ ++ if (sftl->writable) ++ sftl->tags.timeout = 600U * HZ; + disk = blk_mq_alloc_disk(&sftl->tags, &limits, sftl); + if (IS_ERR(disk)) { + ret = PTR_ERR(disk); +@@ -157,13 +241,14 @@ static int sftl_disk_probe(struct platform_device *pdev) + disk->private_data = sftl; + snprintf(disk->disk_name, DISK_NAME_LEN, "applesftl%d", id); + set_capacity(disk, (sector_t)info.disk_pages * (SFTL_LOGICAL_BYTES / SECTOR_SIZE)); +- set_disk_ro(disk, true); ++ set_disk_ro(disk, !sftl->writable); + platform_set_drvdata(pdev, sftl); + ret = device_add_disk(&pdev->dev, disk, NULL); + if (ret) + goto put_disk; +- dev_info(&pdev->dev, "%s: read-only, %u logical pages, sequence %llu\n", +- disk->disk_name, info.disk_pages, info.sequence); ++ dev_info(&pdev->dev, "%s: %s, %u logical pages, sequence %llu\n", ++ disk->disk_name, sftl->writable ? "read-write" : "read-only", ++ info.disk_pages, info.sequence); + return 0; + + put_disk: +@@ -229,6 +314,6 @@ static void __exit sftl_exit(void) + } + module_exit(sftl_exit); + +-MODULE_DESCRIPTION("Apple VFL/SwissPPNFTL read-only block driver"); ++MODULE_DESCRIPTION("Apple VFL/SwissPPNFTL block driver"); + MODULE_LICENSE("GPL"); + MODULE_ALIAS("platform:" SFTL_BLOCK_NAME); +diff --git a/drivers/mtd/apple-sftl-core.c b/drivers/mtd/apple-sftl-core.c +index ba90916f2efe..5d10d72e5f0d 100644 +--- a/drivers/mtd/apple-sftl-core.c ++++ b/drivers/mtd/apple-sftl-core.c +@@ -1,14 +1,15 @@ + // SPDX-License-Identifier: GPL-2.0-only + /* +- * Apple VFL/SwissPPNFTL read-only recovery for the measured 64 GB PPN layout. ++ * Apple VFL/SwissPPNFTL recovery and append for the measured 64 GB PPN layout. + * + * Format and replay rules are ported from Orchard's P105 Python reference +- * (ftl_map.py, ftl_load.py and ftl_log.py). That implementation was checked ++ * (ftl_map.py, ftl_load.py, ftl_log.py and ftl_append.py). It was checked + * against native iOS logical reads. This code contains no captured firmware + * or device data. Unsupported formats fail before a block device is created. + */ + #include + #include ++#include + #include + #include + #include +@@ -34,6 +35,9 @@ + #define SFTL_META 16U + #define SFTL_CHUNK 4096U + #define SFTL_TRIM 0xffff0001U ++#define SFTL_FOOTER_CHUNKS 32U ++#define SFTL_CLAIM_TIMEOUT (500U * HZ) ++#define SFTL_FOOTER_BYTES (SFTL_FOOTER_CHUNKS * SFTL_CHUNK) + + struct sftl_run { + struct rb_node node; +@@ -62,7 +66,15 @@ struct apple_sftl { + u32 nr_runs; + u16 lanes[SFTL_BLOCKS]; + DECLARE_BITMAP(reserved, SFTL_BLOCKS); +- u64 checkpoint_sequence, sequence; ++ DECLARE_BITMAP(vfl_reserved, SFTL_BLOCKS); ++ u64 checkpoint_sequence, sequence, journal_sequence; ++ DECLARE_BITMAP(candidates, SFTL_BLOCKS); ++ DECLARE_BITMAP(used, SFTL_BLOCKS); ++ u32 next_candidate, append_block, append_position, append_capacity; ++ u32 footer_runs; ++ u64 append_base; ++ u8 *footer; ++ bool writer_prepared; + u8 *page; + u8 oob[APPLE_PPN_OOB_SIZE]; + struct mutex lock; /* serializes reads and the shared page buffer */ +@@ -375,6 +387,7 @@ static int sftl_vfl(struct apple_sftl *sftl) + ret = -EBADMSG; + goto out; + } ++ __set_bit(members[member].block, sftl->vfl_reserved); + } + for (member = 0; member < 8; member++) { + for (page = 0; page < 64; page++) { +@@ -646,7 +659,7 @@ static int sftl_footer(struct apple_sftl *sftl, const u8 *head, u32 block, + span = get_unaligned_le32(footer_meta + 8); + if (chunks < 4 || chunks > 32 || chunks % 4 || footer_meta[1] >> 1 != chunks || + get_unaligned_le16(footer_meta + 14) != 0xffff || +- base != sftl_sequence(head) || !span || base + span > SFTL_SEQUENCE) ++ base != sftl_sequence(head) || !span || base + span > SFTL_SEQUENCE + 1) + return -EBADMSG; + *floor = max(*floor, base + span - 1); + if (base + span - 1 <= sftl->checkpoint_sequence) +@@ -722,6 +735,25 @@ static int sftl_footer(struct apple_sftl *sftl, const u8 *head, u32 block, + return ret; + } + ++static bool sftl_partial_footer(struct apple_sftl *sftl, const u8 *head, ++ u32 position, u32 pages) ++{ ++ const u8 *meta = sftl->oob; ++ unsigned int chunk; ++ ++ if (position < pages - SFTL_FOOTER_CHUNKS / 4 || ++ meta[0] != 0x1c || meta[1] != SFTL_FOOTER_CHUNKS * 2 || ++ sftl_sequence(meta) != sftl_sequence(head) || ++ get_unaligned_le32(meta + 8) != pages * 4 - SFTL_FOOTER_CHUNKS || ++ get_unaligned_le16(meta + 12) != SFTL_FOOTER_CHUNKS || ++ get_unaligned_le16(meta + 14) != 0xffff) ++ return false; ++ for (chunk = 1; chunk < 4; chunk++) ++ if (memcmp(meta, sftl->oob + chunk * SFTL_META, SFTL_META)) ++ return false; ++ return true; ++} ++ + static int sftl_journal(struct apple_sftl *sftl, const u8 *heads) + { + struct sftl_records journal = {}; +@@ -732,6 +764,7 @@ static int sftl_journal(struct apple_sftl *sftl, const u8 *heads) + + for (block = 1; block < SFTL_SUBBLOCKS; block++) { + const u8 *head = heads + block * SFTL_META; ++ bool complete_prefix = true, partial_footer = false; + + if (head[0] != 1 && head[0] != 2) + continue; +@@ -759,6 +792,33 @@ static int sftl_journal(struct apple_sftl *sftl, const u8 *heads) + ret = -EBADMSG; + goto out; + } ++ if (position < pages - SFTL_FOOTER_CHUNKS / 4) { ++ for (chunk = 0; chunk < 4; chunk++) { ++ const u8 *meta = sftl->oob + chunk * SFTL_META; ++ u64 sequence = sftl_sequence(head) + position * 4 + chunk; ++ ++ if (meta[0] != 1 || meta[1] || ++ sftl_sequence(meta) != sequence || ++ get_unaligned_le32(meta + 12) != sequence >> 16) ++ complete_prefix = false; ++ } ++ } ++ /* An incomplete final summary leaves the last page erased. ++ * Its full contiguous data prefix must still validate directly. ++ * The partially written summary payload is never trusted. ++ */ ++ if (sftl_partial_footer(sftl, head, position, pages)) { ++ if (!complete_prefix) { ++ ret = -EBADMSG; ++ goto out; ++ } ++ partial_footer = true; ++ continue; ++ } ++ if (partial_footer && !sftl_erased(sftl->oob, sizeof(sftl->oob))) { ++ ret = -EBADMSG; ++ goto out; ++ } + for (chunk = 0; chunk < 4; chunk++) { + ret = sftl_add_journal(&journal, sftl->oob + chunk * SFTL_META, + block * SFTL_STRIDE + position * 4 + chunk, +@@ -808,6 +868,7 @@ static int sftl_journal(struct apple_sftl *sftl, const u8 *heads) + } + sftl->sequence = sequence; + } ++ sftl->journal_sequence = expected - 1; + sftl->sequence = max(sftl->sequence, floor); + ret = 0; + out: +@@ -835,6 +896,8 @@ int apple_sftl_load(struct apple_ppn *provider, struct apple_sftl **out) + return -ENOMEM; + sftl->ppn = provider; + sftl->runs = RB_ROOT; ++ sftl->append_block = U32_MAX; ++ sftl->next_candidate = 1; + mutex_init(&sftl->lock); + sftl->page = kmalloc(APPLE_PPN_PAGE_SIZE, GFP_KERNEL); + heads = kcalloc(SFTL_BLOCKS, SFTL_META, GFP_KERNEL); +@@ -857,6 +920,7 @@ int apple_sftl_load(struct apple_ppn *provider, struct apple_sftl **out) + ret = -EBADMSG; + goto failed; + } ++ __set_bit(block, sftl->candidates); + } else if (sftl->oob[0] == 0x1f || sftl->oob[0] == 1 || sftl->oob[0] == 2) { + memcpy(heads + block * SFTL_META, sftl->oob, SFTL_META); + } else { +@@ -890,6 +954,7 @@ void apple_sftl_free(struct apple_sftl *sftl) + rb_erase(node, &sftl->runs); + kfree(rb_entry(node, struct sftl_run, node)); + } ++ kvfree(sftl->footer); + kfree(sftl->page); + kfree(sftl); + } +@@ -938,6 +1003,242 @@ int apple_sftl_read(struct apple_sftl *sftl, u32 logical_page, u8 *data) + } + EXPORT_SYMBOL_GPL(apple_sftl_read); + ++/* Caller holds sftl->lock. Every candidate is checked in full before use. */ ++static int sftl_writer_prepare(struct apple_sftl *sftl) ++{ ++ struct rb_node *node; ++ u32 block; ++ ++ if (sftl->writer_prepared) ++ return 0; ++ if (sftl->journal_sequence != sftl->sequence) ++ return -EOPNOTSUPP; ++ if (!sftl->ppn->ops->program_page) ++ return -EROFS; ++ sftl->footer = kvzalloc(SFTL_FOOTER_BYTES, GFP_KERNEL); ++ if (!sftl->footer) ++ return -ENOMEM; ++ for (block = 0; block < SFTL_BLOCKS; block++) ++ if (test_bit(block, sftl->reserved) || test_bit(block, sftl->vfl_reserved)) ++ __set_bit(block, sftl->used); ++ __set_bit(0, sftl->used); ++ __set_bit(SFTL_SUBBLOCKS, sftl->used); ++ __set_bit(SFTL_SUBBLOCKS + 1, sftl->used); ++ for (node = rb_first(&sftl->runs); node; node = rb_next(node)) { ++ struct sftl_run *run = rb_entry(node, struct sftl_run, node); ++ u32 end; ++ ++ if (run->provider == SFTL_UNMAPPED) ++ continue; ++ end = (run->provider + run->end - run->start - 1) / SFTL_STRIDE; ++ for (block = run->provider / SFTL_STRIDE; block <= end; block++) ++ __set_bit(block, sftl->used); ++ } ++ sftl->writer_prepared = true; ++ return 0; ++} ++ ++static int sftl_claim_block(struct apple_sftl *sftl) ++{ ++ unsigned long deadline = jiffies + SFTL_CLAIM_TIMEOUT; ++ u32 block; ++ ++ while (sftl->next_candidate < SFTL_SUBBLOCKS) { ++ u32 capacity, position; ++ unsigned int chunk; ++ bool blank = true; ++ int ret; ++ ++ block = sftl->next_candidate++; ++ if (!test_bit(block, sftl->candidates) || ++ test_bit(block, sftl->used) || !sftl->lanes[block]) ++ continue; ++ /* A checked or rejected candidate is never reused in this instance. */ ++ __set_bit(block, sftl->used); ++ capacity = hweight16(sftl->lanes[block]) * 128 * 4; ++ for (position = 0; position < capacity; position += 4) { ++ if (time_after_eq(jiffies, deadline)) { ++ sftl->failed = true; ++ return -ETIMEDOUT; ++ } ++ ret = sftl_read_address(sftl, block * SFTL_STRIDE + position, &chunk); ++ if (ret) { ++ sftl->failed = true; ++ return ret; ++ } ++ if (!sftl_erased(sftl->page, APPLE_PPN_PAGE_SIZE) || ++ !sftl_erased(sftl->oob, APPLE_PPN_OOB_SIZE)) { ++ blank = false; ++ break; ++ } ++ } ++ if (!blank) ++ continue; ++ sftl->append_block = block; ++ sftl->append_capacity = capacity - SFTL_FOOTER_CHUNKS; ++ sftl->append_position = 0; ++ sftl->append_base = sftl->sequence + 1; ++ sftl->footer_runs = 0; ++ memset(sftl->footer, 0, SFTL_FOOTER_BYTES); ++ return 0; ++ } ++ return -ENOSPC; ++} ++ ++static int sftl_program_address(struct apple_sftl *sftl, u32 address, ++ const u8 *data, const u8 *oob) ++{ ++ unsigned int bus, cau, block, page, chunk; ++ int ret; ++ ++ ret = sftl_translate(sftl, address, &bus, &cau, &block, &page, &chunk); ++ if (ret) ++ return ret; ++ if (chunk) ++ return -EINVAL; ++ return sftl->ppn->ops->program_page(sftl->ppn->priv, bus, cau, ++ block, page, data, oob); ++} ++ ++/* A short request repeats its final logical page to fill the program unit. */ ++static void sftl_data_meta(u8 *meta, u64 sequence, u32 logical_page) ++{ ++ put_unaligned_le64(sequence << 16 | 1, meta); ++ put_unaligned_le32(logical_page, meta + 8); ++ put_unaligned_le32(sequence >> 16, meta + 12); ++} ++ ++static void sftl_append_summary(struct apple_sftl *sftl, u64 sequence, u32 key) ++{ ++ u32 relative = sequence - sftl->append_base; ++ u32 high = sequence >> 16; ++ u8 *entry; ++ ++ if (sftl->footer_runs) { ++ u32 length; ++ ++ entry = sftl->footer + (sftl->footer_runs - 1) * SFTL_META; ++ length = get_unaligned_le32(entry + 12); ++ if (key < 0xffff0000 && get_unaligned_le32(entry + 4) == high && ++ get_unaligned_le32(entry) + length == relative && ++ get_unaligned_le32(entry + 8) + length == key) { ++ put_unaligned_le32(length + 1, entry + 12); ++ return; ++ } ++ } ++ entry = sftl->footer + sftl->footer_runs++ * SFTL_META; ++ put_unaligned_le32(relative, entry); ++ put_unaligned_le32(high, entry + 4); ++ put_unaligned_le32(key, entry + 8); ++ put_unaligned_le32(1, entry + 12); ++} ++ ++static int sftl_close_block(struct apple_sftl *sftl) ++{ ++ u8 meta[SFTL_META]; ++ unsigned int chunk, index; ++ int ret; ++ ++ put_unaligned_le64(sftl->append_base << 16 | 0x401c, meta); ++ put_unaligned_le32(sftl->append_position, meta + 8); ++ put_unaligned_le16(SFTL_FOOTER_CHUNKS, meta + 12); ++ put_unaligned_le16(0xffff, meta + 14); ++ for (chunk = 0; chunk < 4; chunk++) ++ memcpy(sftl->oob + chunk * SFTL_META, meta, SFTL_META); ++ for (index = 0; index < SFTL_FOOTER_CHUNKS / 4; index++) { ++ u32 address = sftl->append_block * SFTL_STRIDE + ++ sftl->append_capacity + index * 4; ++ ++ ret = sftl_program_address(sftl, address, ++ sftl->footer + index * APPLE_PPN_PAGE_SIZE, ++ sftl->oob); ++ if (ret) ++ return ret; ++ } ++ sftl->append_block = U32_MAX; ++ return 0; ++} ++ ++int apple_sftl_write(struct apple_sftl *sftl, u32 logical_page, ++ const u8 *data, unsigned int count) ++{ ++ u64 first_sequence; ++ u32 address; ++ unsigned int chunk; ++ int ret; ++ ++ if (!sftl || !data || !count || count > 4 || logical_page >= SFTL_DISK_PAGES || ++ count > SFTL_DISK_PAGES - logical_page) ++ return -EINVAL; ++ mutex_lock(&sftl->lock); ++ if (sftl->failed) { ++ ret = -EIO; ++ goto out; ++ } ++ if (sftl->sequence > SFTL_SEQUENCE - 4) { ++ ret = -EOVERFLOW; ++ goto out; ++ } ++ ret = sftl_writer_prepare(sftl); ++ if (ret) ++ goto out; ++ if (sftl->append_block == U32_MAX) { ++ ret = sftl_claim_block(sftl); ++ if (ret) ++ goto out; ++ } ++ address = sftl->append_block * SFTL_STRIDE + sftl->append_position; ++ first_sequence = sftl->sequence + 1; ++ for (chunk = 0; chunk < 4; chunk++) { ++ unsigned int index = min(chunk, count - 1); ++ ++ memcpy(sftl->page + chunk * SFTL_CHUNK, ++ data + index * SFTL_CHUNK, SFTL_CHUNK); ++ sftl_data_meta(sftl->oob + chunk * SFTL_META, ++ first_sequence + chunk, logical_page + index); ++ } ++ ret = sftl_program_address(sftl, address, sftl->page, sftl->oob); ++ if (ret) ++ goto failed; ++ for (chunk = 0; chunk < 4; chunk++) { ++ u32 key = logical_page + min(chunk, count - 1); ++ ++ ret = sftl_replace(sftl, key, 1, address + chunk); ++ if (ret) ++ goto failed; ++ sftl_append_summary(sftl, first_sequence + chunk, key); ++ } ++ sftl->sequence += 4; ++ sftl->journal_sequence = sftl->sequence; ++ sftl->append_position += 4; ++ if (sftl->append_position == sftl->append_capacity) { ++ ret = sftl_close_block(sftl); ++ if (ret) ++ goto failed; ++ } ++ goto out; ++failed: ++ sftl->failed = true; ++out: ++ mutex_unlock(&sftl->lock); ++ return ret; ++} ++EXPORT_SYMBOL_GPL(apple_sftl_write); ++ ++int apple_sftl_flush(struct apple_sftl *sftl) ++{ ++ int ret; ++ ++ if (!sftl) ++ return -EINVAL; ++ mutex_lock(&sftl->lock); ++ /* Each program is complete and verified before apple_sftl_write returns. */ ++ ret = sftl->failed ? -EIO : 0; ++ mutex_unlock(&sftl->lock); ++ return ret; ++} ++EXPORT_SYMBOL_GPL(apple_sftl_flush); ++ + void apple_sftl_get_info(struct apple_sftl *sftl, struct apple_sftl_info *info) + { + info->logical_pages = SFTL_LPAGES; +@@ -945,5 +1246,6 @@ void apple_sftl_get_info(struct apple_sftl *sftl, struct apple_sftl_info *info) + info->nr_runs = sftl->nr_runs; + info->checkpoint_sequence = sftl->checkpoint_sequence; + info->sequence = sftl->sequence; ++ info->journal_sequence = sftl->journal_sequence; + } + EXPORT_SYMBOL_GPL(apple_sftl_get_info); +diff --git a/drivers/mtd/apple-sftl-test.c b/drivers/mtd/apple-sftl-test.c +index e451704c9ab5..91454af2d7af 100644 +--- a/drivers/mtd/apple-sftl-test.c ++++ b/drivers/mtd/apple-sftl-test.c +@@ -36,10 +36,28 @@ enum sftl_fault { + SFTL_RUNTIME_FAILURE, + }; + ++struct sftl_test_page { ++ u8 data[APPLE_PPN_PAGE_SIZE]; ++ u8 oob[APPLE_PPN_OOB_SIZE]; ++}; ++ + struct sftl_fixture { ++ struct kunit *test; + struct apple_ppn provider; + enum sftl_fault fault; + unsigned int reads; ++ unsigned int programs; ++ unsigned int fail_program; ++ unsigned int tear_program; ++ unsigned int program_outside; ++ bool uncertain_program; ++ bool no_candidates; ++ bool dirty_candidate; ++ bool vfl_candidate; ++ bool full_lanes; ++ u16 program_lanes; ++ u64 sequence_bias; ++ struct sftl_test_page **written[5]; + }; + + static void sftl_test_meta(u8 *meta, u8 kind, u8 flags, u64 seq, u32 key) +@@ -54,7 +72,7 @@ static void sftl_test_meta(u8 *meta, u8 kind, u8 flags, u64 seq, u32 key) + put_unaligned_le32(seq >> 16, meta + 12); + } + +-static void sftl_test_members(u8 *data, enum sftl_fault fault) ++static void sftl_test_members(struct sftl_fixture *f, u8 *data) + { + unsigned int i; + +@@ -63,8 +81,10 @@ static void sftl_test_members(u8 *data, enum sftl_fault fault) + data[0x102 + i * 4] = i; + data[0x103 + i * 4] = 0; + } +- if (fault == SFTL_BOOTSTRAP_DUPLICATE) ++ if (f->fault == SFTL_BOOTSTRAP_DUPLICATE) + data[0x106] = 0; ++ if (f->vfl_candidate) ++ put_unaligned_le16(2056, data + 0x100); + } + + static int sftl_test_context(struct sftl_fixture *f, unsigned int lane, +@@ -79,7 +99,7 @@ static int sftl_test_context(struct sftl_fixture *f, unsigned int lane, + put_unaligned_le16(16, data + 10); + put_unaligned_le16(2086, data + 12); + put_unaligned_le16(2086, data + 14); +- sftl_test_members(data, f->fault); ++ sftl_test_members(f, data); + for (i = 0; i < 16; i++) + put_unaligned_le16(i + 1, data + 0x200 + i * 32); + oob[0] = 0x20; +@@ -95,6 +115,16 @@ static int sftl_test_context(struct sftl_fixture *f, unsigned int lane, + if (page <= 2) + for (i = 1; i <= 4; i++) + put_unaligned_le16(0, data + i * 2); ++ /* Three initially blank append candidates, single-lane by default. */ ++ if (page == 1 && !f->no_candidates) ++ for (i = 5; i <= 7; i++) ++ put_unaligned_le16(0, data + i * 2); ++ if (f->full_lanes) ++ put_unaligned_le16(0, data + 5 * 2); ++ if (page == 1 && f->vfl_candidate) { ++ put_unaligned_le16(0, data + 2056 * 2); ++ put_unaligned_le16(0, data + 2057 * 2); ++ } + oob[0] = 0x20; + oob[1] = page - 1; + oob[2] = 0; +@@ -115,7 +145,7 @@ static void sftl_test_checkpoint(struct sftl_fixture *f, u8 *data, u8 *oob) + memset(data, 0, 4096); + put_unaligned_le32(1, data); + put_unaligned_le32(1, data + 4); +- sftl_test_meta(oob, 0x1f, 1, 100, 0); ++ sftl_test_meta(oob, 0x1f, 1, 100 + f->sequence_bias, 0); + if (f->fault == SFTL_INCOMPLETE_CONTEXT) + return; + if (f->fault == SFTL_INACTIVE_ADDRESS) +@@ -126,12 +156,12 @@ static void sftl_test_checkpoint(struct sftl_fixture *f, u8 *data, u8 *oob) + put_unaligned_le32(1, ranges + 12); + put_unaligned_le32(0x1ffffff, ranges + 16); + put_unaligned_le32(TEST_LOGICAL_PAGES - 1, ranges + 20); +- sftl_test_meta(oob + 16, 0x1f, 4, TEST_CHECKPOINT, 0); ++ sftl_test_meta(oob + 16, 0x1f, 4, TEST_CHECKPOINT + f->sequence_bias, 0); + } + + static void sftl_test_open(struct sftl_fixture *f, u8 *data, u8 *oob) + { +- u64 first = TEST_OPEN_FIRST; ++ u64 first = TEST_OPEN_FIRST + f->sequence_bias; + unsigned int i; + + if (f->fault == SFTL_JOURNAL_GAP) +@@ -145,7 +175,7 @@ static void sftl_test_open(struct sftl_fixture *f, u8 *data, u8 *oob) + if (f->fault == SFTL_TRIM) { + memset(data, 0, 4096); + put_unaligned_le32(1, data); +- put_unaligned_le32(0, data + 4); ++ put_unaligned_le32((TEST_CLOSED_FIRST + f->sequence_bias) >> 16, data + 4); + put_unaligned_le32(1, data + 8); + sftl_test_meta(oob, 2, 0, first, 0xffff0001); + } +@@ -162,19 +192,19 @@ static void sftl_test_closed(struct sftl_fixture *f, unsigned int position, + + memset(data + i * 4096, (slot + 1) & 0xff, 4096); + sftl_test_meta(oob + i * 16, 1, 0, +- TEST_CLOSED_FIRST + slot, slot + 1); ++ TEST_CLOSED_FIRST + f->sequence_bias + slot, slot + 1); + } + } else { + u32 span = TEST_CLOSED_CHUNKS; + + memset(data, 0, 16384); + put_unaligned_le32(0, data); +- put_unaligned_le32(0, data + 4); ++ put_unaligned_le32((TEST_CLOSED_FIRST + f->sequence_bias) >> 16, data + 4); + put_unaligned_le32(1, data + 8); + put_unaligned_le32(TEST_CLOSED_CHUNKS, data + 12); + if (f->fault == SFTL_ALLOCATION_FLOOR) + span += 8; +- sftl_test_meta(oob, 0x1c, 8, TEST_CLOSED_FIRST, span); ++ sftl_test_meta(oob, 0x1c, 8, TEST_CLOSED_FIRST + f->sequence_bias, span); + put_unaligned_le16(f->fault == SFTL_BAD_FOOTER ? 2 : 4, oob + 12); + put_unaligned_le16(0xffff, oob + 14); + for (i = 1; i < 4; i++) +@@ -184,6 +214,24 @@ static void sftl_test_closed(struct sftl_fixture *f, unsigned int position, + } + } + ++static int sftl_test_candidate_slot(unsigned int block) ++{ ++ if (block >= 5 && block <= 7) ++ return block - 5; ++ if (block == 2056 || block == 2057) ++ return block - 2053; ++ return -1; ++} ++ ++static struct sftl_test_page *sftl_test_written(struct sftl_fixture *f, ++ unsigned int slot, unsigned int lane, ++ unsigned int page) ++{ ++ if (!f->written[slot]) ++ return NULL; ++ return f->written[slot][lane * 128 + page]; ++} ++ + static int sftl_test_read_page(void *priv, unsigned int bus, unsigned int cau, + unsigned int block, unsigned int page, + bool context, u8 *data, u8 *oob, u8 *status) +@@ -191,6 +239,7 @@ static int sftl_test_read_page(void *priv, unsigned int bus, unsigned int cau, + struct sftl_fixture *f = priv; + unsigned int lane = cau * 2 + bus; + unsigned int position = page * 2 + lane; ++ int slot = sftl_test_candidate_slot(block); + + f->reads++; + if (bus >= 2 || cau >= 8 || block >= 2088 || page >= (context ? 64 : 128)) +@@ -199,13 +248,26 @@ static int sftl_test_read_page(void *priv, unsigned int bus, unsigned int cau, + memset(oob, 0xff, 64); + *status = 0x49; + if (context) { +- if (block == 2087) ++ if (block == 2087 || (f->vfl_candidate && block == 2056 && !lane)) + return sftl_test_context(f, lane, page, data, oob, status); + return 0; + } + if (block == 3 && (f->fault == SFTL_RUNTIME_FAILURE || + (!position && f->fault == SFTL_READ_FAILURE))) + return -EIO; ++ if (f->dirty_candidate && block == 5 && !lane && page == 127) { ++ memset(data, 'D', APPLE_PPN_PAGE_SIZE); ++ *status = 0x40; ++ return 0; ++ } ++ if (slot >= 0 && sftl_test_written(f, slot, lane, page)) { ++ struct sftl_test_page *written = sftl_test_written(f, slot, lane, page); ++ ++ memcpy(data, written->data, APPLE_PPN_PAGE_SIZE); ++ memcpy(oob, written->oob, APPLE_PPN_OOB_SIZE); ++ *status = 0x40; ++ return 0; ++ } + if (lane > 1) + return 0; + if (block == 1 && !position) { +@@ -217,13 +279,14 @@ static int sftl_test_read_page(void *priv, unsigned int bus, unsigned int cau, + + for (i = 0; i < 4; i++) { + memset(data + i * 4096, 'H' + i, 4096); +- sftl_test_meta(oob + i * 16, 1, 0, TEST_OPEN_FIRST + 4 + i, 3); ++ sftl_test_meta(oob + i * 16, 1, 0, ++ TEST_OPEN_FIRST + f->sequence_bias + 4 + i, 3); + } + } else if (block == 3) { + sftl_test_closed(f, position, data, oob); + } else if (block == 4 && !position) { + memset(data, 'C', 4096); +- sftl_test_meta(oob, 1, 0, 10, 0); ++ sftl_test_meta(oob, 1, 0, 10 + f->sequence_bias, 0); + } else { + return 0; + } +@@ -233,8 +296,50 @@ static int sftl_test_read_page(void *priv, unsigned int bus, unsigned int cau, + return 0; + } + ++static int sftl_test_program_page(void *priv, unsigned int bus, unsigned int cau, ++ unsigned int block, unsigned int page, ++ const u8 *data, const u8 *oob) ++{ ++ struct sftl_fixture *f = priv; ++ struct sftl_test_page *written; ++ unsigned int lane = cau * 2 + bus; ++ int slot = sftl_test_candidate_slot(block); ++ bool failure; ++ ++ f->programs++; ++ if (bus >= 2 || cau >= 8 || slot < 0 || page >= 128 || ++ (lane && (!f->full_lanes || block != 5))) { ++ f->program_outside++; ++ return -EPERM; ++ } ++ if (sftl_test_written(f, slot, lane, page)) ++ return -EEXIST; ++ failure = f->programs == f->fail_program; ++ if (failure && !f->uncertain_program) ++ return -EIO; ++ if (!f->written[slot]) { ++ f->written[slot] = kunit_kcalloc(f->test, 16 * 128, ++ sizeof(*f->written[slot]), GFP_KERNEL); ++ if (!f->written[slot]) ++ return -ENOMEM; ++ } ++ written = kunit_kmalloc(f->test, sizeof(*written), GFP_KERNEL); ++ if (!written) ++ return -ENOMEM; ++ memcpy(written->data, data, APPLE_PPN_PAGE_SIZE); ++ memcpy(written->oob, oob, APPLE_PPN_OOB_SIZE); ++ f->written[slot][lane * 128 + page] = written; ++ f->program_lanes |= 1U << lane; ++ if (f->programs == f->tear_program) { ++ written->oob[63] ^= 1; ++ return -EIO; ++ } ++ return failure ? -EIO : 0; ++} ++ + static const struct apple_ppn_ops sftl_test_ops = { + .read_page = sftl_test_read_page, ++ .program_page = sftl_test_program_page, + }; + + static struct sftl_fixture *sftl_test_fixture(struct kunit *test, +@@ -244,6 +349,7 @@ static struct sftl_fixture *sftl_test_fixture(struct kunit *test, + + if (!f) + return NULL; ++ f->test = test; + f->fault = fault; + f->provider.ops = &sftl_test_ops; + f->provider.priv = f; +@@ -414,7 +520,464 @@ static void sftl_test_runtime_failure_stops_reads(struct kunit *test) + apple_sftl_free(sftl); + } + ++#define TEST_APPEND_FIRST (TEST_OPEN_FIRST + 4) ++#define TEST_APPEND_BASE 2000U ++#define TEST_APPEND_PROGRAMS 120U ++ ++static void sftl_test_append_replay_open(struct kunit *test) ++{ ++ struct sftl_fixture *f = sftl_test_fixture(test, SFTL_NO_FAULT); ++ struct apple_sftl *sftl = NULL; ++ struct apple_sftl_info info; ++ u8 *data = kunit_kmalloc(test, 16384, GFP_KERNEL); ++ u8 *readback = kunit_kmalloc(test, 4096, GFP_KERNEL); ++ unsigned int chunk; ++ ++ KUNIT_ASSERT_NOT_NULL(test, f); ++ KUNIT_ASSERT_NOT_NULL(test, data); ++ KUNIT_ASSERT_NOT_NULL(test, readback); ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ memset(data, 'W', 4096); ++ KUNIT_ASSERT_EQ(test, apple_sftl_write(sftl, TEST_APPEND_BASE, data, 1), 0); ++ for (chunk = 0; chunk < 3; chunk++) ++ memset(data + chunk * 4096, 'X' + chunk, 4096); ++ KUNIT_ASSERT_EQ(test, apple_sftl_write(sftl, TEST_APPEND_BASE + 1, data, 3), 0); ++ KUNIT_EXPECT_EQ(test, apple_sftl_flush(sftl), 0); ++ KUNIT_EXPECT_EQ(test, f->programs, 2U); ++ KUNIT_EXPECT_EQ(test, f->program_outside, 0U); ++ KUNIT_ASSERT_NOT_NULL(test, sftl_test_written(f, 0, 0, 0)); ++ for (chunk = 0; chunk < 4; chunk++) { ++ const u8 *meta = sftl_test_written(f, 0, 0, 0)->oob + chunk * 16; ++ ++ KUNIT_EXPECT_EQ(test, meta[0], (u8)1); ++ KUNIT_EXPECT_EQ(test, get_unaligned_le64(meta) >> 16, ++ TEST_APPEND_FIRST + chunk); ++ KUNIT_EXPECT_EQ(test, get_unaligned_le32(meta + 8), TEST_APPEND_BASE); ++ KUNIT_EXPECT_EQ(test, get_unaligned_le32(meta + 12), ++ (u32)((TEST_APPEND_FIRST + chunk) >> 16)); ++ } ++ apple_sftl_free(sftl); ++ sftl = NULL; ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ apple_sftl_get_info(sftl, &info); ++ KUNIT_EXPECT_EQ(test, info.sequence, TEST_APPEND_FIRST + 7); ++ for (chunk = 0; chunk < 4; chunk++) { ++ KUNIT_ASSERT_EQ(test, apple_sftl_read(sftl, TEST_APPEND_BASE + chunk, readback), 0); ++ sftl_test_expect_fill(test, readback, 'W' + chunk); ++ } ++ KUNIT_ASSERT_EQ(test, apple_sftl_read(sftl, 0, readback), 0); ++ sftl_test_expect_fill(test, readback, 'C'); ++ KUNIT_ASSERT_EQ(test, apple_sftl_read(sftl, 6, readback), 0); ++ sftl_test_expect_fill(test, readback, 6); ++ apple_sftl_free(sftl); ++} ++ ++static int sftl_test_fill_append(struct apple_sftl *sftl, u8 *data) ++{ ++ unsigned int program; ++ int ret; ++ ++ for (program = 0; program < TEST_APPEND_PROGRAMS; program++) { ++ memset(data, program + 1, 16384); ++ ret = apple_sftl_write(sftl, TEST_APPEND_BASE + program * 4, data, 4); ++ if (ret) ++ return ret; ++ } ++ return 0; ++} ++ ++static void sftl_test_append_replay_closed(struct kunit *test) ++{ ++ struct sftl_fixture *f = sftl_test_fixture(test, SFTL_NO_FAULT); ++ struct apple_sftl *sftl = NULL; ++ struct apple_sftl_info info; ++ u8 *data = kunit_kmalloc(test, 16384, GFP_KERNEL); ++ u8 *readback = kunit_kmalloc(test, 4096, GFP_KERNEL); ++ unsigned int page; ++ ++ KUNIT_ASSERT_NOT_NULL(test, f); ++ KUNIT_ASSERT_NOT_NULL(test, data); ++ KUNIT_ASSERT_NOT_NULL(test, readback); ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ KUNIT_ASSERT_EQ(test, sftl_test_fill_append(sftl, data), 0); ++ KUNIT_EXPECT_EQ(test, f->programs, 128U); ++ KUNIT_EXPECT_EQ(test, f->program_outside, 0U); ++ for (page = 120; page < 128; page++) { ++ const u8 *meta; ++ ++ KUNIT_ASSERT_NOT_NULL(test, sftl_test_written(f, 0, 0, page)); ++ meta = sftl_test_written(f, 0, 0, page)->oob; ++ KUNIT_EXPECT_EQ(test, meta[0], (u8)0x1c); ++ KUNIT_EXPECT_EQ(test, meta[1], (u8)64); ++ KUNIT_EXPECT_EQ(test, get_unaligned_le64(meta) >> 16, TEST_APPEND_FIRST); ++ KUNIT_EXPECT_EQ(test, get_unaligned_le32(meta + 8), 480U); ++ KUNIT_EXPECT_EQ(test, get_unaligned_le16(meta + 12), (u16)32); ++ KUNIT_EXPECT_EQ(test, get_unaligned_le16(meta + 14), (u16)0xffff); ++ KUNIT_EXPECT_MEMEQ(test, meta, meta + 16, 16); ++ KUNIT_EXPECT_MEMEQ(test, meta, meta + 32, 16); ++ KUNIT_EXPECT_MEMEQ(test, meta, meta + 48, 16); ++ } ++ KUNIT_EXPECT_EQ(test, get_unaligned_le32(sftl_test_written(f, 0, 0, 120)->data + 8), ++ TEST_APPEND_BASE); ++ KUNIT_EXPECT_EQ(test, get_unaligned_le32(sftl_test_written(f, 0, 0, 120)->data + 12), 480U); ++ apple_sftl_free(sftl); ++ sftl = NULL; ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ apple_sftl_get_info(sftl, &info); ++ KUNIT_EXPECT_EQ(test, info.sequence, TEST_APPEND_FIRST + 479); ++ KUNIT_ASSERT_EQ(test, apple_sftl_read(sftl, TEST_APPEND_BASE, readback), 0); ++ sftl_test_expect_fill(test, readback, 1); ++ KUNIT_ASSERT_EQ(test, apple_sftl_read(sftl, TEST_APPEND_BASE + 479, readback), 0); ++ sftl_test_expect_fill(test, readback, 120); ++ apple_sftl_free(sftl); ++} ++ ++/* Repeating one key prevents summary coalescing: all 8160 data records ++ * need separate entries, including those spanning each footer page. ++ */ ++static void sftl_test_append_replay_full_lanes(struct kunit *test) ++{ ++ struct sftl_fixture *f = sftl_test_fixture(test, SFTL_NO_FAULT); ++ struct apple_sftl *sftl = NULL; ++ struct apple_sftl_info info; ++ const unsigned int programs = 16 * 128 - 8; ++ const unsigned int chunks = programs * 4; ++ u8 *data = kunit_kmalloc(test, 4096, GFP_KERNEL); ++ unsigned int program, lane, page, entry; ++ ++ KUNIT_ASSERT_NOT_NULL(test, f); ++ KUNIT_ASSERT_NOT_NULL(test, data); ++ f->full_lanes = true; ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ for (program = 0; program < programs; program++) { ++ memset(data, program + 1, 4096); ++ KUNIT_ASSERT_EQ(test, apple_sftl_write(sftl, TEST_APPEND_BASE, data, 1), 0); ++ } ++ KUNIT_EXPECT_EQ(test, apple_sftl_flush(sftl), 0); ++ KUNIT_EXPECT_EQ(test, f->programs, 16U * 128); ++ KUNIT_EXPECT_EQ(test, f->program_outside, 0U); ++ KUNIT_EXPECT_EQ(test, f->program_lanes, (u16)0xffff); ++ for (page = 0; page < 128; page++) { ++ for (lane = 0; lane < 16; lane++) { ++ struct sftl_test_page *written = sftl_test_written(f, 0, lane, page); ++ unsigned int position = page * 16 + lane; ++ unsigned int chunk; ++ ++ KUNIT_ASSERT_NOT_NULL(test, written); ++ if (position >= programs) { ++ const u8 *meta = written->oob; ++ ++ KUNIT_EXPECT_EQ(test, meta[0], (u8)0x1c); ++ KUNIT_EXPECT_EQ(test, meta[1], (u8)64); ++ KUNIT_EXPECT_EQ(test, get_unaligned_le64(meta) >> 16, ++ TEST_APPEND_FIRST); ++ KUNIT_EXPECT_EQ(test, get_unaligned_le32(meta + 8), chunks); ++ KUNIT_EXPECT_EQ(test, get_unaligned_le16(meta + 12), (u16)32); ++ KUNIT_EXPECT_EQ(test, get_unaligned_le16(meta + 14), (u16)0xffff); ++ for (chunk = 1; chunk < 4; chunk++) ++ KUNIT_EXPECT_MEMEQ(test, meta, meta + chunk * 16, 16); ++ continue; ++ } ++ for (chunk = 0; chunk < 4; chunk++) { ++ const u8 *meta = written->oob + chunk * 16; ++ u64 sequence = TEST_APPEND_FIRST + position * 4 + chunk; ++ ++ KUNIT_EXPECT_EQ(test, meta[0], (u8)1); ++ KUNIT_EXPECT_EQ(test, meta[1], (u8)0); ++ KUNIT_EXPECT_EQ(test, get_unaligned_le64(meta) >> 16, sequence); ++ KUNIT_EXPECT_EQ(test, get_unaligned_le32(meta + 8), ++ TEST_APPEND_BASE); ++ KUNIT_EXPECT_EQ(test, get_unaligned_le32(meta + 12), ++ (u32)(sequence >> 16)); ++ sftl_test_expect_fill(test, written->data + chunk * 4096, ++ (u8)(position + 1)); ++ } ++ } ++ } ++ for (entry = 0; entry < chunks; entry++) { ++ struct sftl_test_page *footer = sftl_test_written(f, 0, 8 + entry / 1024, 127); ++ const u8 *run = footer->data + (entry % 1024) * 16; ++ ++ KUNIT_EXPECT_EQ(test, get_unaligned_le32(run), entry); ++ KUNIT_EXPECT_EQ(test, get_unaligned_le32(run + 4), ++ (u32)((TEST_APPEND_FIRST + entry) >> 16)); ++ KUNIT_EXPECT_EQ(test, get_unaligned_le32(run + 8), TEST_APPEND_BASE); ++ KUNIT_EXPECT_EQ(test, get_unaligned_le32(run + 12), 1U); ++ } ++ KUNIT_EXPECT_EQ(test, get_unaligned_le32(sftl_test_written(f, 0, 15, 127)->data + ++ (chunks % 1024) * 16 + 12), 0U); ++ apple_sftl_free(sftl); ++ sftl = NULL; ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ apple_sftl_get_info(sftl, &info); ++ KUNIT_EXPECT_EQ(test, info.sequence, TEST_APPEND_FIRST + chunks - 1); ++ KUNIT_ASSERT_EQ(test, apple_sftl_read(sftl, TEST_APPEND_BASE, data), 0); ++ sftl_test_expect_fill(test, data, (u8)programs); ++ KUNIT_ASSERT_EQ(test, apple_sftl_read(sftl, 0, data), 0); ++ sftl_test_expect_fill(test, data, 'C'); ++ apple_sftl_free(sftl); ++} ++ ++static void sftl_test_append_failure_stops_io(struct kunit *test) ++{ ++ struct sftl_fixture *f = sftl_test_fixture(test, SFTL_NO_FAULT); ++ struct apple_sftl *sftl = NULL; ++ u8 *data = kunit_kmalloc(test, 4096, GFP_KERNEL); ++ unsigned int programs; ++ ++ KUNIT_ASSERT_NOT_NULL(test, f); ++ KUNIT_ASSERT_NOT_NULL(test, data); ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ f->fail_program = 1; ++ memset(data, 'F', 4096); ++ KUNIT_EXPECT_EQ(test, apple_sftl_write(sftl, TEST_APPEND_BASE, data, 1), -EIO); ++ programs = f->programs; ++ KUNIT_EXPECT_EQ(test, apple_sftl_write(sftl, TEST_APPEND_BASE, data, 1), -EIO); ++ KUNIT_EXPECT_EQ(test, apple_sftl_flush(sftl), -EIO); ++ KUNIT_EXPECT_EQ(test, apple_sftl_read(sftl, 0, data), -EIO); ++ KUNIT_EXPECT_EQ(test, f->programs, programs); ++ KUNIT_EXPECT_EQ(test, f->program_outside, 0U); ++ apple_sftl_free(sftl); ++ sftl = NULL; ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ KUNIT_ASSERT_EQ(test, apple_sftl_read(sftl, TEST_APPEND_BASE, data), 0); ++ sftl_test_expect_fill(test, data, 0); ++ apple_sftl_free(sftl); ++} ++ ++static void sftl_test_append_uncertain_program_replays(struct kunit *test) ++{ ++ struct sftl_fixture *f = sftl_test_fixture(test, SFTL_NO_FAULT); ++ struct apple_sftl *sftl = NULL; ++ u8 *data = kunit_kmalloc(test, 4096, GFP_KERNEL); ++ ++ KUNIT_ASSERT_NOT_NULL(test, f); ++ KUNIT_ASSERT_NOT_NULL(test, data); ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ f->fail_program = 1; ++ f->uncertain_program = true; ++ memset(data, 'U', 4096); ++ KUNIT_EXPECT_EQ(test, apple_sftl_write(sftl, TEST_APPEND_BASE, data, 1), -EIO); ++ KUNIT_EXPECT_EQ(test, apple_sftl_flush(sftl), -EIO); ++ KUNIT_EXPECT_EQ(test, f->programs, 1U); ++ apple_sftl_free(sftl); ++ sftl = NULL; ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ KUNIT_ASSERT_EQ(test, apple_sftl_read(sftl, TEST_APPEND_BASE, data), 0); ++ sftl_test_expect_fill(test, data, 'U'); ++ apple_sftl_free(sftl); ++} ++ ++static void sftl_test_append_interrupted_footer_replays(struct kunit *test) ++{ ++ static const struct { ++ unsigned int program; ++ bool uncertain; ++ } stop[] = {{121, false}, {122, false}, {128, false}, {128, true}}; ++ unsigned int index; ++ ++ for (index = 0; index < ARRAY_SIZE(stop); index++) { ++ struct sftl_fixture *f = sftl_test_fixture(test, SFTL_NO_FAULT); ++ struct apple_sftl *sftl = NULL; ++ u8 *data = kunit_kmalloc(test, 16384, GFP_KERNEL); ++ ++ KUNIT_ASSERT_NOT_NULL(test, f); ++ KUNIT_ASSERT_NOT_NULL(test, data); ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ f->fail_program = stop[index].program; ++ f->uncertain_program = stop[index].uncertain; ++ KUNIT_EXPECT_EQ(test, sftl_test_fill_append(sftl, data), -EIO); ++ KUNIT_EXPECT_EQ(test, f->programs, stop[index].program); ++ KUNIT_EXPECT_EQ(test, apple_sftl_flush(sftl), -EIO); ++ apple_sftl_free(sftl); ++ sftl = NULL; ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ KUNIT_ASSERT_EQ(test, apple_sftl_read(sftl, TEST_APPEND_BASE + 479, data), 0); ++ sftl_test_expect_fill(test, data, 120); ++ apple_sftl_free(sftl); ++ } ++} ++ ++static void sftl_test_append_torn_metadata_rejected(struct kunit *test) ++{ ++ static const unsigned int torn[] = {1, 121}; ++ unsigned int index; ++ ++ for (index = 0; index < ARRAY_SIZE(torn); index++) { ++ struct sftl_fixture *f = sftl_test_fixture(test, SFTL_NO_FAULT); ++ struct apple_sftl *sftl = NULL; ++ u8 *data = kunit_kmalloc(test, 16384, GFP_KERNEL); ++ ++ KUNIT_ASSERT_NOT_NULL(test, f); ++ KUNIT_ASSERT_NOT_NULL(test, data); ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ f->tear_program = torn[index]; ++ memset(data, 'T', 16384); ++ if (torn[index] == 1) ++ KUNIT_EXPECT_EQ(test, ++ apple_sftl_write(sftl, TEST_APPEND_BASE, data, 1), -EIO); ++ else ++ KUNIT_EXPECT_EQ(test, sftl_test_fill_append(sftl, data), -EIO); ++ apple_sftl_free(sftl); ++ sftl = NULL; ++ KUNIT_EXPECT_LT(test, apple_sftl_load(&f->provider, &sftl), 0); ++ KUNIT_EXPECT_PTR_EQ(test, sftl, NULL); ++ apple_sftl_free(sftl); ++ } ++} ++ ++static void sftl_test_append_scans_entire_candidate(struct kunit *test) ++{ ++ struct sftl_fixture *f = sftl_test_fixture(test, SFTL_NO_FAULT); ++ struct apple_sftl *sftl = NULL; ++ u8 *data = kunit_kmalloc(test, 4096, GFP_KERNEL); ++ ++ KUNIT_ASSERT_NOT_NULL(test, f); ++ KUNIT_ASSERT_NOT_NULL(test, data); ++ f->dirty_candidate = true; ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ memset(data, 'B', 4096); ++ KUNIT_ASSERT_EQ(test, apple_sftl_write(sftl, TEST_APPEND_BASE, data, 1), 0); ++ KUNIT_EXPECT_PTR_EQ(test, sftl_test_written(f, 0, 0, 0), NULL); ++ KUNIT_ASSERT_NOT_NULL(test, sftl_test_written(f, 1, 0, 0)); ++ KUNIT_EXPECT_EQ(test, f->programs, 1U); ++ KUNIT_EXPECT_EQ(test, f->program_outside, 0U); ++ apple_sftl_free(sftl); ++ sftl = NULL; ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ KUNIT_ASSERT_EQ(test, apple_sftl_read(sftl, TEST_APPEND_BASE, data), 0); ++ sftl_test_expect_fill(test, data, 'B'); ++ apple_sftl_free(sftl); ++} ++ ++static void sftl_test_append_reserves_vfl_members(struct kunit *test) ++{ ++ struct sftl_fixture *f = sftl_test_fixture(test, SFTL_NO_FAULT); ++ struct apple_sftl *sftl = NULL; ++ u8 *data = kunit_kmalloc(test, 4096, GFP_KERNEL); ++ ++ KUNIT_ASSERT_NOT_NULL(test, f); ++ KUNIT_ASSERT_NOT_NULL(test, data); ++ f->vfl_candidate = true; ++ f->no_candidates = true; ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ memset(data, 'V', 4096); ++ KUNIT_ASSERT_EQ(test, apple_sftl_write(sftl, TEST_APPEND_BASE, data, 1), 0); ++ KUNIT_EXPECT_PTR_EQ(test, sftl_test_written(f, 3, 0, 0), NULL); ++ KUNIT_ASSERT_NOT_NULL(test, sftl_test_written(f, 4, 0, 0)); ++ KUNIT_EXPECT_EQ(test, f->programs, 1U); ++ KUNIT_EXPECT_EQ(test, f->program_outside, 0U); ++ apple_sftl_free(sftl); ++ sftl = NULL; ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ KUNIT_ASSERT_EQ(test, apple_sftl_read(sftl, TEST_APPEND_BASE, data), 0); ++ sftl_test_expect_fill(test, data, 'V'); ++ apple_sftl_free(sftl); ++} ++ ++static void sftl_test_append_closes_at_sequence_limit(struct kunit *test) ++{ ++ struct sftl_fixture *f = sftl_test_fixture(test, SFTL_NO_FAULT); ++ struct apple_sftl *sftl = NULL; ++ struct apple_sftl_info info; ++ const u64 limit = (1ULL << 48) - 1; ++ u8 *data = kunit_kmalloc(test, 16384, GFP_KERNEL); ++ unsigned int programs; ++ ++ KUNIT_ASSERT_NOT_NULL(test, f); ++ KUNIT_ASSERT_NOT_NULL(test, data); ++ f->sequence_bias = limit - TEST_APPEND_FIRST - 479; ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ KUNIT_ASSERT_EQ(test, sftl_test_fill_append(sftl, data), 0); ++ programs = f->programs; ++ KUNIT_EXPECT_EQ(test, programs, 128U); ++ apple_sftl_get_info(sftl, &info); ++ KUNIT_EXPECT_EQ(test, info.sequence, limit); ++ KUNIT_EXPECT_EQ(test, apple_sftl_write(sftl, TEST_APPEND_BASE, data, 1), -EOVERFLOW); ++ KUNIT_EXPECT_EQ(test, f->programs, programs); ++ KUNIT_EXPECT_EQ(test, apple_sftl_flush(sftl), 0); ++ apple_sftl_free(sftl); ++ sftl = NULL; ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ apple_sftl_get_info(sftl, &info); ++ KUNIT_EXPECT_EQ(test, info.sequence, limit); ++ KUNIT_ASSERT_EQ(test, apple_sftl_read(sftl, TEST_APPEND_BASE + 479, data), 0); ++ sftl_test_expect_fill(test, data, 120); ++ KUNIT_EXPECT_EQ(test, apple_sftl_write(sftl, TEST_APPEND_BASE, data, 1), -EOVERFLOW); ++ KUNIT_EXPECT_EQ(test, f->programs, programs); ++ apple_sftl_free(sftl); ++} ++ ++static void sftl_test_append_no_space_keeps_reads(struct kunit *test) ++{ ++ struct sftl_fixture *f = sftl_test_fixture(test, SFTL_NO_FAULT); ++ struct apple_sftl *sftl = NULL; ++ u8 *data = kunit_kmalloc(test, 4096, GFP_KERNEL); ++ ++ KUNIT_ASSERT_NOT_NULL(test, f); ++ KUNIT_ASSERT_NOT_NULL(test, data); ++ f->no_candidates = true; ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ memset(data, 'N', 4096); ++ KUNIT_EXPECT_EQ(test, apple_sftl_write(sftl, TEST_APPEND_BASE, data, 1), -ENOSPC); ++ KUNIT_EXPECT_EQ(test, f->programs, 0U); ++ KUNIT_EXPECT_EQ(test, apple_sftl_read(sftl, 0, data), 0); ++ sftl_test_expect_fill(test, data, 'C'); ++ apple_sftl_free(sftl); ++} ++ ++static void sftl_test_append_refuses_sequence_floor_gap(struct kunit *test) ++{ ++ struct sftl_fixture *f = sftl_test_fixture(test, SFTL_ALLOCATION_FLOOR); ++ struct apple_sftl *sftl = NULL; ++ u8 *data = kunit_kmalloc(test, 4096, GFP_KERNEL); ++ ++ KUNIT_ASSERT_NOT_NULL(test, f); ++ KUNIT_ASSERT_NOT_NULL(test, data); ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ memset(data, 'S', 4096); ++ KUNIT_EXPECT_EQ(test, apple_sftl_write(sftl, TEST_APPEND_BASE, data, 1), -EOPNOTSUPP); ++ KUNIT_EXPECT_EQ(test, f->programs, 0U); ++ KUNIT_EXPECT_EQ(test, apple_sftl_read(sftl, 0, data), 0); ++ sftl_test_expect_fill(test, data, 'C'); ++ apple_sftl_free(sftl); ++} ++ ++static void sftl_test_append_rejects_invalid_window(struct kunit *test) ++{ ++ struct sftl_fixture *f = sftl_test_fixture(test, SFTL_NO_FAULT); ++ struct apple_sftl *sftl = NULL; ++ u8 *data = kunit_kmalloc(test, 16384, GFP_KERNEL); ++ unsigned int reads; ++ ++ KUNIT_ASSERT_NOT_NULL(test, f); ++ KUNIT_ASSERT_NOT_NULL(test, data); ++ KUNIT_ASSERT_EQ(test, apple_sftl_load(&f->provider, &sftl), 0); ++ reads = f->reads; ++ KUNIT_EXPECT_EQ(test, apple_sftl_write(sftl, TEST_APPEND_BASE, data, 0), -EINVAL); ++ KUNIT_EXPECT_EQ(test, apple_sftl_write(sftl, TEST_APPEND_BASE, data, 5), -EINVAL); ++ KUNIT_EXPECT_EQ(test, apple_sftl_write(sftl, 15625000, data, 1), -EINVAL); ++ KUNIT_EXPECT_EQ(test, apple_sftl_write(sftl, 15624999, data, 2), -EINVAL); ++ KUNIT_EXPECT_EQ(test, apple_sftl_write(sftl, TEST_APPEND_BASE, NULL, 1), -EINVAL); ++ KUNIT_EXPECT_EQ(test, f->reads, reads); ++ KUNIT_EXPECT_EQ(test, f->programs, 0U); ++ apple_sftl_free(sftl); ++} ++ + static struct kunit_case apple_sftl_cases[] = { ++ KUNIT_CASE(sftl_test_append_replay_open), ++ KUNIT_CASE(sftl_test_append_replay_closed), ++ KUNIT_CASE(sftl_test_append_replay_full_lanes), ++ KUNIT_CASE(sftl_test_append_failure_stops_io), ++ KUNIT_CASE(sftl_test_append_uncertain_program_replays), ++ KUNIT_CASE(sftl_test_append_interrupted_footer_replays), ++ KUNIT_CASE(sftl_test_append_torn_metadata_rejected), ++ KUNIT_CASE(sftl_test_append_scans_entire_candidate), ++ KUNIT_CASE(sftl_test_append_reserves_vfl_members), ++ KUNIT_CASE(sftl_test_append_closes_at_sequence_limit), ++ KUNIT_CASE(sftl_test_append_no_space_keeps_reads), ++ KUNIT_CASE(sftl_test_append_refuses_sequence_floor_gap), ++ KUNIT_CASE(sftl_test_append_rejects_invalid_window), + KUNIT_CASE(sftl_test_complete_medium), + KUNIT_CASE(sftl_test_scan_after_open_lane_hole), + KUNIT_CASE(sftl_test_runtime_failure_stops_reads), +diff --git a/drivers/mtd/apple-sftl.h b/drivers/mtd/apple-sftl.h +index 7a05b95d1109..0babcebb0418 100644 +--- a/drivers/mtd/apple-sftl.h ++++ b/drivers/mtd/apple-sftl.h +@@ -13,11 +13,15 @@ struct apple_sftl_info { + u32 nr_runs; + u64 checkpoint_sequence; + u64 sequence; ++ u64 journal_sequence; + }; + + int apple_sftl_load(struct apple_ppn *provider, struct apple_sftl **out); + void apple_sftl_free(struct apple_sftl *sftl); + int apple_sftl_read(struct apple_sftl *sftl, u32 logical_page, u8 *data); ++int apple_sftl_write(struct apple_sftl *sftl, u32 logical_page, ++ const u8 *data, unsigned int count); ++int apple_sftl_flush(struct apple_sftl *sftl); + void apple_sftl_get_info(struct apple_sftl *sftl, struct apple_sftl_info *info); + + #endif