diff --git a/devices/p105/README.md b/devices/p105/README.md index dfac9ea9..cde705f6 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), 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. @@ -48,3 +49,7 @@ 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. +- #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 new file mode 100644 index 00000000..146b88f6 --- /dev/null +++ b/docs/bring-up/p105-sftl-append.md @@ -0,0 +1,79 @@ +# 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. + +## Operating policy + +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 + +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` +is `untested` until a device test. diff --git a/docs/bring-up/p105-sftl-reader.md b/docs/bring-up/p105-sftl-reader.md new file mode 100644 index 00000000..7a8b2cad --- /dev/null +++ b/docs/bring-up/p105-sftl-reader.md @@ -0,0 +1,61 @@ +# 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`. + +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 + +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 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` 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 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