From 3a03552c6ad13d835d2dd62e358d9ffd82b161c5 Mon Sep 17 00:00:00 2001 From: Marco Casaroli Date: Sat, 25 Jul 2026 13:53:16 +0200 Subject: [PATCH] testing/fs/xipfs: Add a test suite for the xipfs file system. Twelve sections, selected by name on the command line because the power loss sweeps run for minutes while everything else runs in seconds. The routine part covers the VFS paths, the write-once rules (reopen for write, append, seek during write, truncate of a written file are all refused) and both mmap variants: that a plain mapping lands inside the media window with no heap growth, that MAP_XIP_STRICT fails with ENXIO rather than copying, and that N mappings of one file produce N pins on one extent. The rest is aimed at the two properties that are easy to get wrong and quiet when they are: Pin release. A pin taken by one task and a task that dies with a mapping still live both have to end with the extent movable again -- the second without the task ever calling munmap, since a module that faults never will. Both are checked by asking the defragmenter to move the extent afterwards. Power-loss atomicity. With CONFIG_FS_XIPFS_FAULT_INJECT the suite fails the Nth flash operation, remounts, and checks the volume is consistent and every file that had been committed is byte-for-byte intact. It sweeps N across create, unlink and defragmentation, and repeats the sweep with the failing write torn -- a partial program rather than a clean refusal -- which is what a real power loss mid-program leaves behind. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Marco Casaroli --- testing/fs/xipfs/CMakeLists.txt | 33 + testing/fs/xipfs/Kconfig | 38 + testing/fs/xipfs/Make.defs | 25 + testing/fs/xipfs/Makefile | 34 + testing/fs/xipfs/xipfs_main.c | 2034 +++++++++++++++++++++++++++++++ 5 files changed, 2164 insertions(+) create mode 100644 testing/fs/xipfs/CMakeLists.txt create mode 100644 testing/fs/xipfs/Kconfig create mode 100644 testing/fs/xipfs/Make.defs create mode 100644 testing/fs/xipfs/Makefile create mode 100644 testing/fs/xipfs/xipfs_main.c diff --git a/testing/fs/xipfs/CMakeLists.txt b/testing/fs/xipfs/CMakeLists.txt new file mode 100644 index 000000000..720224f73 --- /dev/null +++ b/testing/fs/xipfs/CMakeLists.txt @@ -0,0 +1,33 @@ +# ############################################################################## +# apps/testing/fs/xipfs/CMakeLists.txt +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more contributor +# license agreements. See the NOTICE file distributed with this work for +# additional information regarding copyright ownership. The ASF licenses this +# file to you under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# +# ############################################################################## + +if(CONFIG_TESTING_FS_XIPFS) + nuttx_add_application( + NAME + xipfs_test + STACKSIZE + ${CONFIG_DEFAULT_TASK_STACKSIZE} + MODULE + ${CONFIG_TESTING_FS_XIPFS} + SRCS + xipfs_main.c) +endif() diff --git a/testing/fs/xipfs/Kconfig b/testing/fs/xipfs/Kconfig new file mode 100644 index 000000000..8d4350d9a --- /dev/null +++ b/testing/fs/xipfs/Kconfig @@ -0,0 +1,38 @@ +# +# For a description of the syntax of this configuration file, +# see the file kconfig-language.txt in the NuttX tools repository. +# + +config TESTING_FS_XIPFS + tristate "XIPFS file system test" + depends on FS_XIPFS + default n + ---help--- + Exercise the XIPFS filesystem: the routine VFS paths, the + write-once rules, the in-place mmap and its strict variant, and + then the corners that actually carry risk -- power loss atomicity + of the metadata commit, and release of XIP pins when a task dies + without unmapping. + + Takes an optional section name to run just one part, since the + power loss sweeps take minutes and everything else takes seconds: + + xipfs_test everything + xipfs_test xip just the execute-in-place mapping tests + + Sections: basic writeonce strict xip multimap crosstask teardown + dirs defrag powerloss powertorn unlink dirloss defraglos + + The power loss sections need CONFIG_FS_XIPFS_FAULT_INJECT. + +if TESTING_FS_XIPFS + +config TESTING_FS_XIPFS_MOUNTPT + string "XIPFS mountpoint" + default "/mnt/xipfs" + +config TESTING_FS_XIPFS_MTD + string "MTD device backing the filesystem" + default "/dev/rammtd" + +endif # TESTING_FS_XIPFS diff --git a/testing/fs/xipfs/Make.defs b/testing/fs/xipfs/Make.defs new file mode 100644 index 000000000..7eeca601c --- /dev/null +++ b/testing/fs/xipfs/Make.defs @@ -0,0 +1,25 @@ +############################################################################ +# apps/testing/fs/xipfs/Make.defs +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +ifneq ($(CONFIG_TESTING_FS_XIPFS),) +CONFIGURED_APPS += $(APPDIR)/testing/fs/xipfs +endif diff --git a/testing/fs/xipfs/Makefile b/testing/fs/xipfs/Makefile new file mode 100644 index 000000000..45cdeb83d --- /dev/null +++ b/testing/fs/xipfs/Makefile @@ -0,0 +1,34 @@ +############################################################################ +# apps/testing/fs/xipfs/Makefile +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +include $(APPDIR)/Make.defs + +# XIPFS file system test + +PROGNAME = xipfs_test +PRIORITY = SCHED_PRIORITY_DEFAULT +STACKSIZE = $(CONFIG_DEFAULT_TASK_STACKSIZE) +MODULE = $(CONFIG_TESTING_FS_XIPFS) + +MAINSRC = xipfs_main.c + +include $(APPDIR)/Application.mk diff --git a/testing/fs/xipfs/xipfs_main.c b/testing/fs/xipfs/xipfs_main.c new file mode 100644 index 000000000..6f2ad2612 --- /dev/null +++ b/testing/fs/xipfs/xipfs_main.c @@ -0,0 +1,2034 @@ +/**************************************************************************** + * apps/testing/fs/xipfs/xipfs_main.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define MOUNTPT CONFIG_TESTING_FS_XIPFS_MOUNTPT +#define MTDDEV CONFIG_TESTING_FS_XIPFS_MTD + +#define PATH(name) MOUNTPT "/" name + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +static int g_passed; +static int g_failed; +static FAR uint8_t *g_buffer; +static size_t g_bufsize = 16384; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +static void report(FAR const char *name, bool ok, FAR const char *detail) +{ + if (ok) + { + g_passed++; + printf(" PASS %s\n", name); + } + else + { + g_failed++; + printf(" FAIL %s: %s\n", name, detail ? detail : ""); + } +} + +#define CHECK(name, cond, detail) report((name), (cond), (detail)) + +/**************************************************************************** + * Name: remount + * + * Description: + * Discard every scrap of in-RAM filesystem state and mount again from the + * medium. Because the rammtd buffer survives, this is exactly what a + * reboot looks like to the filesystem, which is what makes the power loss + * sweep below meaningful without restarting the simulator. + * + ****************************************************************************/ + +static int remount(void) +{ + int ret; + + ret = umount(MOUNTPT); + if (ret < 0 && errno != ENOENT && errno != EINVAL) + { + printf(" umount failed: %d\n", errno); + return -errno; + } + + ret = mount(MTDDEV, MOUNTPT, "xipfs", 0, NULL); + if (ret < 0) + { + return -errno; + } + + return 0; +} + +/**************************************************************************** + * Name: fill_pattern + ****************************************************************************/ + +static void fill_pattern(FAR uint8_t *buf, size_t len, uint8_t seed) +{ + size_t i; + + for (i = 0; i < len; i++) + { + buf[i] = (uint8_t)(seed + (i * 7)); + } +} + +/**************************************************************************** + * Name: create_file + * + * Description: + * Create a file of a known size. ftruncate is how the exact size is + * declared up front, which is what lets the filesystem reserve the exact + * contiguous extent rather than over-reserving and trimming. + * + ****************************************************************************/ + +static int create_file(FAR const char *path, size_t len, uint8_t seed) +{ + ssize_t nwritten; + int fd; + int ret = 0; + + fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + if (fd < 0) + { + return -errno; + } + + if (ftruncate(fd, len) < 0) + { + ret = -errno; + goto out; + } + + fill_pattern(g_buffer, len, seed); + + nwritten = write(fd, g_buffer, len); + if (nwritten != (ssize_t)len) + { + ret = nwritten < 0 ? -errno : -EIO; + } + +out: + if (close(fd) < 0 && ret == 0) + { + ret = -errno; + } + + return ret; +} + +/**************************************************************************** + * Name: verify_file + ****************************************************************************/ + +static bool verify_file(FAR const char *path, size_t len, uint8_t seed) +{ + FAR uint8_t *expect; + ssize_t nread; + bool ok = false; + int fd; + + expect = malloc(len); + if (expect == NULL) + { + return false; + } + + fill_pattern(expect, len, seed); + + fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) + { + free(expect); + return false; + } + + nread = read(fd, g_buffer, len + 1); + if (nread == (ssize_t)len && memcmp(g_buffer, expect, len) == 0) + { + ok = true; + } + + close(fd); + free(expect); + return ok; +} + +/**************************************************************************** + * Name: extent_info + ****************************************************************************/ + +static int extent_info(FAR const char *path, + FAR struct xipfs_extent_info_s *info) +{ + int fd; + int ret; + + fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) + { + return -errno; + } + + ret = ioctl(fd, XIPFSIOC_EXTENTINFO, (unsigned long)(uintptr_t)info); + if (ret < 0) + { + ret = -errno; + } + + close(fd); + return ret; +} + +/**************************************************************************** + * Name: run_defrag + ****************************************************************************/ + +/* Defragmentation acts on the volume, so it is issued on a descriptor for + * the mountpoint directory rather than for a file inside it. That matters + * beyond tidiness: a descriptor for a file inside the volume would hold + * that file open, and an open extent cannot be relocated, so the pass would + * be obstructed by the very act of asking for it. + */ + +static int run_defrag(uint32_t max_ms, + FAR struct xipfs_defrag_result_s *result) +{ + struct xipfs_defrag_arg_s arg; + int fd; + int ret; + + memset(&arg, 0, sizeof(arg)); + arg.max_ms = max_ms; + + fd = open(MOUNTPT, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (fd < 0) + { + return -errno; + } + + ret = ioctl(fd, XIPFSIOC_DEFRAG, (unsigned long)(uintptr_t)&arg); + if (ret < 0) + { + ret = -errno; + } + else + { + *result = arg.result; + } + + close(fd); + return ret; +} + +/**************************************************************************** + * Name: arm_injector + * + * Description: + * Arm the fault injector to fail the n'th flash write or erase from now + * on, and every one after it. 'mode' chooses whether the failing + * operation leaves the medium untouched (XIPFS_FAULT_CLEAN) or half-done, + * the way a real cut does (XIPFS_FAULT_TORN). It is a volume command, so + * it goes through the mountpoint directory like run_defrag, which also + * means it can be armed on a volume with no files in it. + * + ****************************************************************************/ + +static int arm_injector(int countdown, uint8_t mode) +{ + struct xipfs_fault_s fault; + int fd; + int ret; + + fault.count = countdown; + fault.mode = mode; + + fd = open(MOUNTPT, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (fd < 0) + { + return -errno; + } + + ret = ioctl(fd, XIPFSIOC_FAULTINJECT, (unsigned long)(uintptr_t)&fault); + if (ret < 0) + { + ret = -errno; + } + + close(fd); + return ret; +} + +/**************************************************************************** + * Test: basic VFS operations and the contiguity invariant + ****************************************************************************/ + +static void test_basic(void) +{ + struct xipfs_extent_info_s info; + struct stat st; + FAR DIR *dirp; + FAR struct dirent *de; + int found = 0; + int ret; + + printf("\n-- basic VFS --\n"); + + ret = create_file(PATH("basic1.bin"), 100, 0x11); + CHECK("create small file", ret == 0, strerror(-ret)); + + ret = create_file(PATH("basic2.bin"), 9000, 0x22); + CHECK("create multi-block file", ret == 0, strerror(-ret)); + + CHECK("read back small", verify_file(PATH("basic1.bin"), 100, 0x11), ""); + CHECK("read back multi-block", + verify_file(PATH("basic2.bin"), 9000, 0x22), ""); + + ret = stat(PATH("basic2.bin"), &st); + CHECK("stat size", ret == 0 && st.st_size == 9000, "wrong size"); + + /* The core invariant: one contiguous, erase-block aligned extent whose + * block count is exactly what the file size requires. + */ + + ret = extent_info(PATH("basic2.bin"), &info); + CHECK("extent is contiguous and exactly sized", + ret == 0 && info.nblocks == + (info.size + info.erasesize - 1) / info.erasesize, + "extent size mismatch"); + + dirp = opendir(MOUNTPT); + if (dirp != NULL) + { + while ((de = readdir(dirp)) != NULL) + { + if (strcmp(de->d_name, "basic1.bin") == 0 || + strcmp(de->d_name, "basic2.bin") == 0) + { + found++; + } + } + + closedir(dirp); + } + + CHECK("readdir lists both files", found == 2, "missing entries"); + + ret = unlink(PATH("basic1.bin")); + CHECK("unlink", ret == 0, strerror(errno)); + CHECK("unlinked file is gone", stat(PATH("basic1.bin"), &st) < 0, ""); + + /* Survives a remount: the directory is on the medium, not in RAM */ + + ret = remount(); + CHECK("remount", ret == 0, strerror(-ret)); + CHECK("data survives remount", + verify_file(PATH("basic2.bin"), 9000, 0x22), ""); + CHECK("delete survives remount", + stat(PATH("basic1.bin"), &st) < 0, ""); + + unlink(PATH("basic2.bin")); +} + +/**************************************************************************** + * Test: execute-in-place mapping + * + * This is the test that proves the entire premise. A mapping must land + * inside the flash window and must not cost RAM proportional to the file. + ****************************************************************************/ + +static void test_xip(void) +{ + struct xipfs_extent_info_s info; + struct mallinfo before; + struct mallinfo after; + const size_t len = 12288; + FAR void *addr; + FAR uint8_t *expect; + int fd; + int ret; + + printf("\n-- XIP mapping --\n"); + + ret = create_file(PATH("xip.bin"), len, 0x33); + CHECK("create module image", ret == 0, strerror(-ret)); + + ret = extent_info(PATH("xip.bin"), &info); + CHECK("media is memory mapped", ret == 0 && info.xipaddr != 0, + "no XIP base"); + + if (info.xipaddr == 0) + { + return; + } + + fd = open(PATH("xip.bin"), O_RDONLY | O_CLOEXEC); + if (fd < 0) + { + CHECK("open for mapping", false, strerror(errno)); + return; + } + + before = mallinfo(); + addr = mmap(NULL, len, PROT_READ | PROT_EXEC, + MAP_SHARED | MAP_XIP_STRICT, fd, 0); + after = mallinfo(); + + CHECK("strict XIP mmap succeeds", addr != MAP_FAILED, strerror(errno)); + + if (addr != MAP_FAILED) + { + /* The returned pointer must BE the flash address, not a copy of it */ + + CHECK("mapping points into flash", + (uintptr_t)addr == info.xipaddr, "not the extent address"); + + expect = malloc(len); + if (expect != NULL) + { + fill_pattern(expect, len, 0x33); + CHECK("mapped bytes match file contents", + memcmp(addr, expect, len) == 0, "content mismatch"); + free(expect); + } + + /* A RAM copy would have cost at least the file size. Allow slack + * for the mapping bookkeeping itself. + */ + + CHECK("mapping does not cost RAM proportional to the file", + (after.uordblks - before.uordblks) < (int)(len / 2), + "RAM grew by about the file size"); + + ret = extent_info(PATH("xip.bin"), &info); + CHECK("mapping takes a pin", ret == 0 && info.pincount == 1, + "pin not taken"); + + ret = munmap(addr, len); + CHECK("munmap", ret == 0, strerror(errno)); + + ret = extent_info(PATH("xip.bin"), &info); + CHECK("munmap drops the pin", ret == 0 && info.pincount == 0, + "pin not released"); + } + + close(fd); + unlink(PATH("xip.bin")); +} + +/**************************************************************************** + * Test: multiple concurrent mappings share one pin count + ****************************************************************************/ + +static void test_multimap(void) +{ + struct xipfs_extent_info_s info; + const size_t len = 8192; + FAR void *addr[3]; + int fd[3]; + int i; + int ret; + + printf("\n-- concurrent mappings --\n"); + + ret = create_file(PATH("multi.bin"), len, 0x44); + CHECK("create", ret == 0, strerror(-ret)); + + for (i = 0; i < 3; i++) + { + fd[i] = open(PATH("multi.bin"), O_RDONLY | O_CLOEXEC); + addr[i] = mmap(NULL, len, PROT_READ, MAP_SHARED | MAP_XIP_STRICT, + fd[i], 0); + } + + ret = extent_info(PATH("multi.bin"), &info); + CHECK("three mappings give a pin count of three", + ret == 0 && info.pincount == 3, "wrong pin count"); + + /* All three must resolve to the same flash address: they alias the same + * extent rather than each getting a private copy. + */ + + CHECK("mappings alias the same flash address", + addr[0] == addr[1] && addr[1] == addr[2], "addresses differ"); + + /* Within one task, all three mappings share an address, so a single + * munmap of that address releases all of them. That is munmap's range + * semantics -- the VFS unmaps every mapping the range covers -- and it + * only arises because true XIP makes the addresses identical. It is not + * the multi-instance case: separate module instances are separate tasks + * with separate mapping lists, which test_crosstask_pins covers. + */ + + munmap(addr[0], len); + ret = extent_info(PATH("multi.bin"), &info); + CHECK("unmapping an aliased address releases that task's mappings", + ret == 0 && info.pincount == 0, "pin leaked"); + + for (i = 0; i < 3; i++) + { + close(fd[i]); + } + + unlink(PATH("multi.bin")); +} + +/**************************************************************************** + * Test: pins are refcounted across tasks + * + * This is the multi-instance case that matters: two tasks each holding a + * live XIP mapping of one module. One of them dying must not release the + * other's pin, or the surviving instance would be executing out of blocks + * the defragmenter now considers movable. + ****************************************************************************/ + +static int holder_task(int argc, FAR char *argv[]) +{ + const size_t len = 8192; + FAR void *addr; + int fd; + + fd = open(PATH("shared.bin"), O_RDONLY | O_CLOEXEC); + if (fd < 0) + { + return EXIT_FAILURE; + } + + addr = mmap(NULL, len, PROT_READ, MAP_SHARED | MAP_XIP_STRICT, fd, 0); + close(fd); + + if (addr == MAP_FAILED) + { + return EXIT_FAILURE; + } + + /* Exit holding the mapping */ + + return EXIT_SUCCESS; +} + +static void test_crosstask_pins(void) +{ + struct xipfs_extent_info_s info; + const size_t len = 8192; + FAR void *addr; + int status; + pid_t pid; + int fd; + int ret; + + printf("\n-- cross-task pin refcounting --\n"); + + ret = create_file(PATH("shared.bin"), len, 0xaa); + CHECK("create", ret == 0, strerror(-ret)); + + fd = open(PATH("shared.bin"), O_RDONLY | O_CLOEXEC); + addr = mmap(NULL, len, PROT_READ, MAP_SHARED | MAP_XIP_STRICT, fd, 0); + CHECK("parent maps", addr != MAP_FAILED, strerror(errno)); + + pid = task_create("holder", SCHED_PRIORITY_DEFAULT, 4096, holder_task, + NULL); + if (pid > 0) + { + waitpid(pid, &status, 0); + usleep(100000); + + /* The child's teardown must have dropped exactly its own pin */ + + ret = extent_info(PATH("shared.bin"), &info); + CHECK("a dying task does not release another task's pin", + ret == 0 && info.pincount == 1, "wrong pin count"); + } + + if (addr != MAP_FAILED) + { + munmap(addr, len); + } + + ret = extent_info(PATH("shared.bin"), &info); + CHECK("last holder releases the extent", + ret == 0 && info.pincount == 0, "pin leaked"); + + close(fd); + unlink(PATH("shared.bin")); +} + +/**************************************************************************** + * Test: a task that dies without unmapping must still release its pin + ****************************************************************************/ + +static int leaky_mapper(int argc, FAR char *argv[]) +{ + const size_t len = 8192; + FAR void *addr; + int fd; + + fd = open(PATH("leak.bin"), O_RDONLY | O_CLOEXEC); + if (fd < 0) + { + return EXIT_FAILURE; + } + + addr = mmap(NULL, len, PROT_READ, MAP_SHARED | MAP_XIP_STRICT, fd, 0); + if (addr == MAP_FAILED) + { + close(fd); + return EXIT_FAILURE; + } + + /* Deliberately exit with the mapping still live and no munmap call. A + * module that faults or is killed does exactly this, and the pin has to + * come back anyway or the extent is immovable forever. + */ + + close(fd); + return EXIT_SUCCESS; +} + +static void test_teardown_release(void) +{ + struct xipfs_extent_info_s info; + int status; + pid_t pid; + int ret; + + printf("\n-- pin release on task exit --\n"); + + ret = create_file(PATH("leak.bin"), 8192, 0x55); + CHECK("create", ret == 0, strerror(-ret)); + + pid = task_create("leaky", SCHED_PRIORITY_DEFAULT, 4096, leaky_mapper, + NULL); + CHECK("spawn mapper task", pid > 0, strerror(errno)); + + if (pid > 0) + { + waitpid(pid, &status, 0); + + /* Give the exiting task's group teardown a chance to run */ + + usleep(100000); + + ret = extent_info(PATH("leak.bin"), &info); + CHECK("pin released although the task never called munmap", + ret == 0 && info.pincount == 0, "pin leaked on task exit"); + } + + unlink(PATH("leak.bin")); +} + +/**************************************************************************** + * Test: directories + * + * Directories are records in the metadata generation, not names with + * separators in them, so what is checked is that they behave like a real + * tree: they can be empty, they survive a remount empty, rmdir refuses a + * directory that still holds something, and a path through a file or a + * missing directory fails the way POSIX says rather than springing into + * existence. + ****************************************************************************/ + +static void test_dirs(void) +{ + struct stat st; + FAR struct dirent *de; + FAR DIR *dirp; + int binseen = 0; + int etcseen = 0; + bool sawtop = false; + bool sawhello = false; + bool sawworld = false; + bool badentry = false; + int fd; + int ret; + + printf("\n-- directories --\n"); + + ret = mkdir(PATH("bin"), 0755); + CHECK("mkdir", ret == 0, strerror(errno)); + + ret = mkdir(PATH("bin"), 0755); + CHECK("mkdir of an existing directory is refused", + ret < 0 && errno == EEXIST, strerror(errno)); + + /* An empty directory is a real thing here, and it has to survive a + * remount with nothing inside it. This is the whole difference from + * synthesising directories out of file names. + */ + + ret = stat(PATH("bin"), &st); + CHECK("an empty directory exists", ret == 0 && S_ISDIR(st.st_mode), + strerror(errno)); + + ret = remount(); + CHECK("remount", ret == 0, strerror(-ret)); + + ret = stat(PATH("bin"), &st); + CHECK("an empty directory survives a remount", + ret == 0 && S_ISDIR(st.st_mode), strerror(errno)); + + /* Files below it, and a directory two levels down */ + + ret = create_file(PATH("bin/hello"), 100, 1); + CHECK("create a file in a directory", ret == 0, strerror(-ret)); + + ret = create_file(PATH("bin/world"), 100, 2); + CHECK("create a second one beside it", ret == 0, strerror(-ret)); + + ret = mkdir(PATH("etc"), 0755); + ret |= mkdir(PATH("etc/conf"), 0755); + CHECK("mkdir two levels down", ret == 0, strerror(errno)); + + ret = create_file(PATH("etc/conf/net"), 100, 3); + CHECK("create a file two levels down", ret == 0, strerror(-ret)); + + ret = create_file(PATH("top.bin"), 100, 4); + CHECK("create one at the root", ret == 0, strerror(-ret)); + + /* A path through a directory that does not exist is an error, not an + * invitation to create it. + */ + + fd = open(PATH("nodir/x"), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + CHECK("a path through a missing directory fails", + fd < 0 && errno == ENOENT, strerror(errno)); + if (fd >= 0) + { + close(fd); + } + + fd = open(PATH("top.bin/deeper"), + O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + CHECK("a path through a file fails", fd < 0 && errno == ENOTDIR, + strerror(errno)); + if (fd >= 0) + { + close(fd); + } + + /* Listing: the root holds two directories and a file, once each */ + + dirp = opendir(MOUNTPT); + while (dirp != NULL && (de = readdir(dirp)) != NULL) + { + if (strcmp(de->d_name, "bin") == 0 && de->d_type == DTYPE_DIRECTORY) + { + binseen++; + } + else if (strcmp(de->d_name, "etc") == 0 && + de->d_type == DTYPE_DIRECTORY) + { + etcseen++; + } + else if (strcmp(de->d_name, "top.bin") == 0 && + de->d_type == DTYPE_FILE) + { + sawtop = true; + } + } + + if (dirp != NULL) + { + closedir(dirp); + } + + CHECK("the root lists its directories and its files", + binseen == 1 && etcseen == 1 && sawtop, "missing entry"); + + dirp = opendir(PATH("bin")); + CHECK("opendir on a subdirectory", dirp != NULL, strerror(errno)); + + while (dirp != NULL && (de = readdir(dirp)) != NULL) + { + if (strchr(de->d_name, '/') != NULL || de->d_type != DTYPE_FILE) + { + badentry = true; + } + else if (strcmp(de->d_name, "hello") == 0) + { + sawhello = true; + } + else if (strcmp(de->d_name, "world") == 0) + { + sawworld = true; + } + } + + if (dirp != NULL) + { + closedir(dirp); + } + + CHECK("it lists its own entries, by name", + sawhello && sawworld && !badentry, "unexpected entries"); + + ret = stat(PATH("bin/hello"), &st); + CHECK("stat of a file in a directory", + ret == 0 && S_ISREG(st.st_mode) && st.st_size == 100, + strerror(errno)); + + /* The VFS passes a trailing separator through, and it names the same + * directory. + */ + + dirp = opendir(PATH("etc/conf/")); + ret = stat(PATH("etc/conf/"), &st); + CHECK("a trailing separator names the same directory", + dirp != NULL && ret == 0 && S_ISDIR(st.st_mode), strerror(errno)); + + if (dirp != NULL) + { + closedir(dirp); + } + + /* "." and ".." are refused rather than interpreted, because an entry by + * either name could never be reached again. + */ + + ret = mkdir(PATH("etc/.."), 0755); + CHECK("a dot-dot component is refused", ret < 0 && errno == EINVAL, + strerror(errno)); + + /* The type rules, in both directions */ + + /* A read-only open of a directory is not an error: the VFS turns the + * EISDIR the file system answers into a directory descriptor, which is + * what makes fdopendir and the volume ioctls work. Opening one for + * writing has nowhere to go, so that is where EISDIR surfaces. + */ + + fd = open(PATH("bin"), O_RDONLY | O_CLOEXEC); + ret = fd >= 0 ? fstat(fd, &st) : -1; + CHECK("a read-only open of a directory yields a directory", + fd >= 0 && ret == 0 && S_ISDIR(st.st_mode), strerror(errno)); + if (fd >= 0) + { + close(fd); + } + + fd = open(PATH("bin"), O_WRONLY | O_CREAT | O_CLOEXEC, 0644); + CHECK("opening a directory for writing is refused", + fd < 0 && errno == EISDIR, strerror(errno)); + if (fd >= 0) + { + close(fd); + } + + ret = unlink(PATH("bin")); + CHECK("unlinking a directory is refused", ret < 0 && errno == EISDIR, + strerror(errno)); + + ret = rmdir(PATH("top.bin")); + CHECK("rmdir of a file is refused", ret < 0 && errno == ENOTDIR, + strerror(errno)); + + ret = rmdir(PATH("bin")); + CHECK("rmdir of a directory with contents is refused", + ret < 0 && errno == ENOTEMPTY, strerror(errno)); + + /* Emptying it makes it removable, and removing it takes nothing else */ + + unlink(PATH("bin/hello")); + unlink(PATH("bin/world")); + + ret = rmdir(PATH("bin")); + CHECK("rmdir of an emptied directory", ret == 0, strerror(errno)); + + ret = stat(PATH("bin"), &st); + CHECK("it is gone", ret < 0 && errno == ENOENT, strerror(errno)); + + ret = stat(PATH("etc/conf/net"), &st); + CHECK("removing one directory leaves the others alone", + ret == 0 && S_ISREG(st.st_mode), strerror(errno)); + + /* And the whole tree is durable */ + + ret = remount(); + CHECK("remount after all of it", ret == 0, strerror(-ret)); + + ret = stat(PATH("etc/conf"), &st); + CHECK("the tree is the same after a remount", + ret == 0 && S_ISDIR(st.st_mode) && + verify_file(PATH("etc/conf/net"), 100, 3), "tree changed"); + + unlink(PATH("etc/conf/net")); + rmdir(PATH("etc/conf")); + rmdir(PATH("etc")); + unlink(PATH("top.bin")); +} + +/**************************************************************************** + * Test: defragmentation + ****************************************************************************/ + +static void test_defrag(void) +{ + struct xipfs_defrag_result_s result; + struct xipfs_extent_info_s info; + const size_t small = 4096; + size_t largest_before; + FAR void *addr; + int fd; + int ret; + int i; + char path[64]; + + printf("\n-- defragmentation --\n"); + + /* Lay down an alternating pattern, then punch out every other file. The + * result is free space that is plentiful in total but useless in any + * single run, which is precisely the situation defrag exists for. + */ + + for (i = 0; i < 10; i++) + { + snprintf(path, sizeof(path), PATH("frag%d.bin"), i); + if (create_file(path, small, (uint8_t)i) < 0) + { + break; + } + } + + for (i = 0; i < 10; i += 2) + { + snprintf(path, sizeof(path), PATH("frag%d.bin"), i); + unlink(path); + } + + ret = run_defrag(0, &result); + CHECK("defrag runs", ret == 0, strerror(-ret)); + largest_before = result.largest_free_run; + + /* Nothing is open, because the pass was asked for through the mountpoint + * directory, so it should have run to completion rather than reporting an + * obstruction. + */ + + CHECK("defrag through the mountpoint directory completes", + result.reason == XIPFS_DEFRAG_DONE, "unexpected reason"); + + /* Surviving files must be byte-identical after being physically moved */ + + for (i = 1; i < 10; i += 2) + { + snprintf(path, sizeof(path), PATH("frag%d.bin"), i); + if (!verify_file(path, small, (uint8_t)i)) + { + break; + } + } + + CHECK("relocated files keep their contents", i >= 10, "content changed"); + + /* Everything must still be there after a remount: each relocation + * committed a new generation naming the new location. + */ + + ret = remount(); + CHECK("remount after defrag", ret == 0, strerror(-ret)); + + for (i = 1; i < 10; i += 2) + { + snprintf(path, sizeof(path), PATH("frag%d.bin"), i); + if (!verify_file(path, small, (uint8_t)i)) + { + break; + } + } + + CHECK("relocations survive remount", i >= 10, "content lost"); + + /* An open file is still an obstruction, and must be reported as one. + * The pass only stops on it when the open extent is the only candidate + * for the hole it is trying to fill, because otherwise it just moves + * something else instead. So arrange exactly that: punch out the + * second-highest file and hold open the highest, which is then the only + * extent above the hole. + */ + + { + uint32_t beststart = 0; + uint32_t nextstart = 0; + char lastpath[64]; + char holepath[64]; + + lastpath[0] = '\0'; + holepath[0] = '\0'; + + for (i = 1; i < 10; i += 2) + { + snprintf(path, sizeof(path), PATH("frag%d.bin"), i); + if (extent_info(path, &info) < 0) + { + continue; + } + + if (lastpath[0] == '\0' || info.start_block > beststart) + { + nextstart = beststart; + strlcpy(holepath, lastpath, sizeof(holepath)); + beststart = info.start_block; + strlcpy(lastpath, path, sizeof(lastpath)); + } + else if (holepath[0] == '\0' || info.start_block > nextstart) + { + nextstart = info.start_block; + strlcpy(holepath, path, sizeof(holepath)); + } + } + + if (lastpath[0] != '\0' && holepath[0] != '\0') + { + unlink(holepath); + + fd = open(lastpath, O_RDONLY | O_CLOEXEC); + if (fd >= 0) + { + ret = run_defrag(0, &result); + CHECK("an open file is reported as blocking, not as done", + ret == 0 && + result.reason == XIPFS_DEFRAG_BLOCKED_OPEN, + "unexpected reason"); + close(fd); + } + + /* Put it back so the pinned test below has the same layout to + * work with as before. + */ + + create_file(holepath, small, 1); + } + } + + /* A pinned extent must be skipped, and defrag must say so rather than + * silently reporting success. + */ + + snprintf(path, sizeof(path), PATH("frag1.bin")); + fd = open(path, O_RDONLY | O_CLOEXEC); + addr = mmap(NULL, small, PROT_READ, MAP_SHARED | MAP_XIP_STRICT, fd, 0); + + if (addr != MAP_FAILED) + { + ret = extent_info(path, &info); + + /* Fragment again below the pinned extent so defrag has a reason to + * want to move it. + */ + + for (i = 3; i < 10; i += 2) + { + snprintf(path, sizeof(path), PATH("frag%d.bin"), i); + unlink(path); + } + + ret = run_defrag(0, &result); + CHECK("defrag with a pinned extent still returns cleanly", ret == 0, + strerror(-ret)); + + snprintf(path, sizeof(path), PATH("frag1.bin")); + ret = extent_info(path, &info); + CHECK("pinned extent was not relocated", + ret == 0 && (uintptr_t)addr == info.xipaddr, + "pinned extent moved under a live mapping"); + + munmap(addr, small); + close(fd); + } + + /* After releasing the pin, a further pass should be able to do at least + * as well as before. + */ + + ret = run_defrag(0, &result); + CHECK("defrag after unpinning", + ret == 0 && result.largest_free_run >= largest_before, + "free space did not recover"); + + for (i = 0; i < 10; i++) + { + snprintf(path, sizeof(path), PATH("frag%d.bin"), i); + unlink(path); + } +} + +/**************************************************************************** + * Test: power loss at every step of a create + * + * The medium survives; only the filesystem's RAM state is discarded, which + * is exactly what a reboot does. For every flash operation in turn, fail + * at that operation and assert that what comes back is a consistent + * filesystem -- never a torn directory and never a half written file. + * + * The sweep runs twice. In XIPFS_FAULT_CLEAN the failing operation leaves + * the medium untouched. In XIPFS_FAULT_TORN it leaves it half done -- half + * a page programmed, half a sector erased -- which is what a real cut leaves + * and what makes the mount-time CRC actually have to reject a half-formed + * generation rather than an obviously blank one. Both must yield the same + * two invariants, because the commit writes the directory body before the + * header that validates it: a torn body is never reachable behind a written + * header. + ****************************************************************************/ + +static void powerloss_create(uint8_t mode, FAR const char *modename) +{ + const int max_ops = 16; + struct stat st; + bool all_ok = true; + bool saw_failure = false; + char detail[80]; + char name[80]; + int ret; + int n; + + printf("\n-- power loss during create (%s) --\n", modename); + + for (n = 0; n < max_ops; n++) + { + ret = remount(); + if (ret < 0) + { + snprintf(detail, sizeof(detail), + "remount failed before injection %d: %d", n, ret); + all_ok = false; + break; + } + + /* A file that must survive every injected failure untouched */ + + unlink(PATH("keep.bin")); + unlink(PATH("victim.bin")); + + if (create_file(PATH("keep.bin"), 2048, 0x66) < 0) + { + snprintf(detail, sizeof(detail), "baseline create failed at %d", + n); + all_ok = false; + break; + } + + /* Arm the injector: fail the n'th flash write or erase from now on */ + + if (arm_injector(n, mode) < 0) + { + all_ok = false; + strlcpy(detail, "cannot open to arm injector", sizeof(detail)); + break; + } + + /* This create will be cut short at some arbitrary point */ + + ret = create_file(PATH("victim.bin"), 5000, 0x77); + if (ret < 0) + { + saw_failure = true; + } + + /* Now "reboot" */ + + ret = remount(); + if (ret < 0) + { + snprintf(detail, sizeof(detail), + "mount failed after injection %d: %d", n, ret); + all_ok = false; + break; + } + + /* Invariant 1: the pre-existing file is untouched. A commit that + * tore would most likely take the whole directory with it. + */ + + if (!verify_file(PATH("keep.bin"), 2048, 0x66)) + { + snprintf(detail, sizeof(detail), + "pre-existing file corrupted by injection %d", n); + all_ok = false; + break; + } + + /* Invariant 2: the interrupted file is either absent or complete. + * There is no valid state in between. + */ + + if (stat(PATH("victim.bin"), &st) == 0) + { + if (st.st_size != 5000 || + !verify_file(PATH("victim.bin"), 5000, 0x77)) + { + snprintf(detail, sizeof(detail), + "partially written file visible after injection %d", + n); + all_ok = false; + break; + } + } + } + + snprintf(name, sizeof(name), + "filesystem stays consistent across every injection point (%s)", + modename); + CHECK(name, all_ok, detail); + + snprintf(name, sizeof(name), + "injection actually interrupted some operations (%s)", modename); + CHECK(name, saw_failure, + "no operation was ever cut short; the sweep proved nothing"); + + remount(); + unlink(PATH("keep.bin")); + unlink(PATH("victim.bin")); +} + +static void test_powerloss(void) +{ + powerloss_create(XIPFS_FAULT_CLEAN, "clean"); +} + +static void test_powerloss_torn(void) +{ + powerloss_create(XIPFS_FAULT_TORN, "torn"); +} + +/**************************************************************************** + * Test: power loss at every step of an unlink + * + * An unlink is a single metadata commit followed by returning the blocks to + * the allocator, so the file is either still there in full or entirely gone. + * What makes it worth sweeping is not the file being deleted but the two + * files either side of it: if a torn commit were to leave the directory + * naming an extent whose blocks the allocator has already handed back, the + * next create would be given live blocks and would quietly overwrite a + * neighbour. That is why each iteration ends by allocating again and then + * re-checking the survivors. + ****************************************************************************/ + +static void test_powerloss_unlink(void) +{ + const int max_ops = 16; + const size_t len = 3000; + struct stat st; + bool all_ok = true; + bool saw_failure = false; + bool saw_survival = false; + bool saw_deletion = false; + bool exhausted = false; + char detail[80]; + int quiet = 0; + int ret; + int n; + + printf("\n-- power loss during unlink --\n"); + detail[0] = '\0'; + + for (n = 0; n < max_ops; n++) + { + ret = remount(); + if (ret < 0) + { + snprintf(detail, sizeof(detail), + "remount failed before injection %d: %d", n, ret); + all_ok = false; + break; + } + + unlink(PATH("ul_a.bin")); + unlink(PATH("ul_b.bin")); + unlink(PATH("ul_c.bin")); + unlink(PATH("ul_new.bin")); + + if (create_file(PATH("ul_a.bin"), len, 0xa1) < 0 || + create_file(PATH("ul_b.bin"), len, 0xb2) < 0 || + create_file(PATH("ul_c.bin"), len, 0xc3) < 0) + { + snprintf(detail, sizeof(detail), + "baseline create failed at %d", n); + all_ok = false; + break; + } + + if (arm_injector(n, XIPFS_FAULT_CLEAN) < 0) + { + strlcpy(detail, "cannot arm injector", sizeof(detail)); + all_ok = false; + break; + } + + /* Delete the middle file. This is the operation being cut short. */ + + if (unlink(PATH("ul_b.bin")) < 0) + { + saw_failure = true; + quiet = 0; + } + else + { + /* The injector was armed further out than this operation reaches. + * Two of those in a row means the sweep has walked off the end of + * the unlink and later points would only be testing the creates + * that follow it. + */ + + if (++quiet >= 2) + { + exhausted = true; + } + } + + /* Reboot */ + + ret = remount(); + if (ret < 0) + { + snprintf(detail, sizeof(detail), + "mount failed after injection %d: %d", n, ret); + all_ok = false; + break; + } + + /* Invariant 1: the neighbours are untouched. They were never part of + * the operation, so nothing about a torn unlink may reach them. + */ + + if (!verify_file(PATH("ul_a.bin"), len, 0xa1) || + !verify_file(PATH("ul_c.bin"), len, 0xc3)) + { + snprintf(detail, sizeof(detail), + "neighbour corrupted by injection %d", n); + all_ok = false; + break; + } + + /* Invariant 2: the target is either wholly gone or wholly intact. + * A directory entry surviving with unreadable or truncated contents + * would mean the commit and the free happened in the wrong order. + */ + + if (stat(PATH("ul_b.bin"), &st) == 0) + { + saw_survival = true; + + if (st.st_size != (off_t)len || + !verify_file(PATH("ul_b.bin"), len, 0xb2)) + { + snprintf(detail, sizeof(detail), + "unlink left a damaged file at injection %d", n); + all_ok = false; + break; + } + } + else + { + saw_deletion = true; + } + + /* Invariant 3: the recovered allocator agrees with the recovered + * directory. Filling the volume and re-checking the survivors is what + * would catch blocks that were freed while still referenced -- the new + * file would be placed on top of a live extent. + */ + + if (create_file(PATH("ul_new.bin"), len, 0x5e) == 0 && + !verify_file(PATH("ul_new.bin"), len, 0x5e)) + { + snprintf(detail, sizeof(detail), + "new file unreadable after injection %d", n); + all_ok = false; + break; + } + + if (!verify_file(PATH("ul_a.bin"), len, 0xa1) || + !verify_file(PATH("ul_c.bin"), len, 0xc3)) + { + snprintf(detail, sizeof(detail), + "allocator reissued live blocks after injection %d", n); + all_ok = false; + break; + } + + if (stat(PATH("ul_b.bin"), &st) == 0 && + !verify_file(PATH("ul_b.bin"), len, 0xb2)) + { + snprintf(detail, sizeof(detail), + "surviving target clobbered after injection %d", n); + all_ok = false; + break; + } + + if (exhausted) + { + break; + } + } + + CHECK("unlink stays consistent across every injection point", all_ok, + detail); + CHECK("unlink injection actually interrupted some operations", saw_failure, + "no unlink was ever cut short; the sweep proved nothing"); + + /* Both outcomes must be reachable. If the sweep only ever saw one of + * them the commit point is not where it is documented to be, and the + * other half of the invariant went untested. + */ + + CHECK("both unlink outcomes observed across the sweep", + saw_survival && saw_deletion, + saw_survival ? "no injection ever completed the delete" + : "no injection ever preserved the file"); + + /* Say plainly whether the sweep covered the whole operation or merely the + * first max_ops of it, so a green result is not mistaken for coverage it + * does not have on a medium with a different geometry. + */ + + printf(" swept %d injection points, %s\n", n, + exhausted ? "covering the whole unlink" + : "stopping at the cap before the unlink ran out of ops"); + + remount(); + unlink(PATH("ul_a.bin")); + unlink(PATH("ul_b.bin")); + unlink(PATH("ul_c.bin")); + unlink(PATH("ul_new.bin")); +} + +/**************************************************************************** + * Test: power loss at every step of a defragmentation pass + * + * This is the sweep that matters most, because defrag is the only operation + * that moves data a user already owns. Everywhere else an interrupted + * operation can at worst discard something that was never complete; here a + * relocation that tore could destroy a file that was safe before the pass + * began. + * + * So the invariant is absolute rather than two-sided: defrag never creates + * or removes a file, so after a power loss at any point during it, every + * file must still be present and byte-identical. There is no acceptable + * "or absent" case. + ****************************************************************************/ + +static void test_powerloss_defrag(void) +{ + /* Each relocation is an erase of the destination, a page-at-a-time copy, + * a metadata commit and an erase of the vacated blocks. How many flash + * operations that comes to depends entirely on the medium's page and + * erase geometry, so rather than guess, the sweep walks injection points + * until two in a row fail to reach the injector at all -- meaning it has + * walked off the end of the pass -- and reports whether it got that far + * before the cap. On rammtd the whole pass is about fifty operations; + * a medium with smaller pages will take more, and the cap is what keeps + * the suite's runtime bounded there. + */ + + const int max_ops = 128; + const int nfiles = 10; + const size_t small = 4096; + struct xipfs_defrag_result_s result; + bool all_ok = true; + bool saw_failure = false; + bool saw_progress = false; + bool exhausted = false; + char detail[80]; + char path[64]; + int quiet = 0; + int ret; + int n; + int i; + + printf("\n-- power loss during defrag --\n"); + detail[0] = '\0'; + + for (n = 0; n < max_ops && all_ok; n++) + { + ret = remount(); + if (ret < 0) + { + snprintf(detail, sizeof(detail), + "remount failed before injection %d: %d", n, ret); + all_ok = false; + break; + } + + /* Rebuild the same fragmented layout every iteration so that a given + * injection point means the same thing each time round. + */ + + for (i = 0; i < nfiles; i++) + { + snprintf(path, sizeof(path), PATH("pd%d.bin"), i); + unlink(path); + } + + for (i = 0; i < nfiles; i++) + { + snprintf(path, sizeof(path), PATH("pd%d.bin"), i); + if (create_file(path, small, (uint8_t)(0x40 + i)) < 0) + { + snprintf(detail, sizeof(detail), + "baseline create %d failed at injection %d", i, n); + all_ok = false; + break; + } + } + + if (!all_ok) + { + break; + } + + /* Punch out every other file: plenty of free space in total, none of + * it in a single run. This is what gives defrag work to do. + */ + + for (i = 0; i < nfiles; i += 2) + { + snprintf(path, sizeof(path), PATH("pd%d.bin"), i); + unlink(path); + } + + if (arm_injector(n, XIPFS_FAULT_CLEAN) < 0) + { + strlcpy(detail, "cannot arm injector", sizeof(detail)); + all_ok = false; + break; + } + + /* Cut power partway through the pass. Note that defrag reports a + * clean stop rather than an error when a relocation fails, so the + * media error surfaces in the reason code, not the return value. + */ + + ret = run_defrag(0, &result); + if (ret < 0) + { + snprintf(detail, sizeof(detail), + "defrag returned an error at injection %d: %d", n, ret); + all_ok = false; + break; + } + + if (result.reason == XIPFS_DEFRAG_ERROR) + { + saw_failure = true; + quiet = 0; + } + else if (++quiet >= 2) + { + /* The pass ran to completion without ever tripping the injector, + * twice over: every operation in it has now been swept. + */ + + exhausted = true; + } + + if (result.extents_moved > 0) + { + saw_progress = true; + } + + /* Rebuilding a ten file layout against real flash takes a couple of + * seconds per injection point, and the sweep is over a hundred of + * them. Without a heartbeat the suite looks wedged for minutes on + * hardware, so a silent run cannot be told from a hung one. + */ + + putchar('.'); + fflush(stdout); + + /* Reboot */ + + ret = remount(); + if (ret < 0) + { + snprintf(detail, sizeof(detail), + "mount failed after injection %d: %d", n, ret); + all_ok = false; + break; + } + + /* The invariant: every survivor is still there, still whole. Whether + * it was relocated, half relocated or never touched must be + * indistinguishable from the outside. + */ + + for (i = 1; i < nfiles; i += 2) + { + snprintf(path, sizeof(path), PATH("pd%d.bin"), i); + if (!verify_file(path, small, (uint8_t)(0x40 + i))) + { + snprintf(detail, sizeof(detail), + "file %d lost or corrupted by injection %d", i, n); + all_ok = false; + break; + } + } + + if (!all_ok) + { + break; + } + + /* And the recovered allocator must not believe the blocks under a + * survivor are free. A relocation that committed but was interrupted + * before erasing its source leaves stale copies behind; those blocks + * are unreferenced and reusable, but the blocks under the new location + * are not, and a mount that confused the two would hand them out here. + */ + + unlink(PATH("pd_new.bin")); + if (create_file(PATH("pd_new.bin"), small, 0x5f) == 0) + { + if (!verify_file(PATH("pd_new.bin"), small, 0x5f)) + { + snprintf(detail, sizeof(detail), + "new file unreadable after injection %d", n); + all_ok = false; + break; + } + + for (i = 1; i < nfiles; i += 2) + { + snprintf(path, sizeof(path), PATH("pd%d.bin"), i); + if (!verify_file(path, small, (uint8_t)(0x40 + i))) + { + snprintf(detail, sizeof(detail), + "allocator reissued live blocks after " + "injection %d", n); + all_ok = false; + break; + } + } + } + + if (exhausted) + { + break; + } + } + + putchar('\n'); + + CHECK("no file is ever lost to a power loss during defrag", all_ok, + detail); + CHECK("defrag injection actually interrupted some relocations", + saw_failure, "no relocation was ever cut short; the sweep " + "proved nothing"); + CHECK("defrag made real progress at some injection points", saw_progress, + "no extent was ever relocated; the sweep never reached a move"); + + printf(" swept %d injection points, %s\n", n, + exhausted ? "covering every operation of the whole pass" + : "stopping at the cap partway through the pass"); + + remount(); + for (i = 0; i < nfiles; i++) + { + snprintf(path, sizeof(path), PATH("pd%d.bin"), i); + unlink(path); + } + + unlink(PATH("pd_new.bin")); +} + +/**************************************************************************** + * Test: strict versus permissive mapping + ****************************************************************************/ + +static void test_strict(void) +{ + const size_t len = 4096; + FAR void *addr; + int fd; + int ret; + + printf("\n-- strict mapping semantics --\n"); + + ret = create_file(PATH("strict.bin"), len, 0x88); + CHECK("create", ret == 0, strerror(-ret)); + + fd = open(PATH("strict.bin"), O_RDONLY | O_CLOEXEC); + if (fd < 0) + { + CHECK("open", false, strerror(errno)); + return; + } + + /* Beyond the end of the file: strict must refuse rather than quietly + * hand back something that is not what was asked for. + */ + + addr = mmap(NULL, len * 4, PROT_READ, MAP_SHARED | MAP_XIP_STRICT, fd, 0); + CHECK("strict mapping past end of file is refused", addr == MAP_FAILED, + "oversized mapping accepted"); + + addr = mmap(NULL, len, PROT_READ, MAP_SHARED | MAP_XIP_STRICT, fd, 0); + CHECK("strict mapping of a valid range succeeds", addr != MAP_FAILED, + strerror(errno)); + + if (addr != MAP_FAILED) + { + munmap(addr, len); + } + + close(fd); + unlink(PATH("strict.bin")); +} + +/**************************************************************************** + * Test: the write-once model is enforced rather than half-implemented + ****************************************************************************/ + +static void test_writeonce(void) +{ + const size_t len = 1024; + ssize_t nwritten; + int fd; + int ret; + + printf("\n-- write-once enforcement --\n"); + + ret = create_file(PATH("once.bin"), len, 0x99); + CHECK("create", ret == 0, strerror(-ret)); + + fd = open(PATH("once.bin"), O_WRONLY | O_CLOEXEC); + CHECK("reopening a closed file for writing is refused", fd < 0, + "file was writable after close"); + if (fd >= 0) + { + close(fd); + } + + fd = open(PATH("once.bin"), O_WRONLY | O_APPEND | O_CLOEXEC); + CHECK("append is refused", fd < 0, "file was appendable"); + if (fd >= 0) + { + close(fd); + } + + /* A seek away from the append point during a create would leave a hole, + * which an exactly sized extent cannot represent. + */ + + fd = open(PATH("hole.bin"), + O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); + if (fd >= 0) + { + ftruncate(fd, len); + ret = lseek(fd, 512, SEEK_SET); + CHECK("seeking while creating is refused", ret < 0, "seek allowed"); + + nwritten = write(fd, g_buffer, 16); + CHECK("sequential write after a rejected seek still works", + nwritten == 16, "write failed"); + close(fd); + } + + unlink(PATH("once.bin")); + unlink(PATH("hole.bin")); +} + +/**************************************************************************** + * Test: power loss during mkdir and rmdir + * + * A directory is a record in the metadata generation, so creating or + * removing one is the same single-generation commit that create and unlink + * are. That is the claim; this sweeps it. For every flash operation in + * turn, fail at that one and assert the volume comes back as a tree with the + * directory either wholly there or wholly absent -- never a record whose + * parent has gone, which is what a partially applied change would look like + * and what mount would then have to reject. + ****************************************************************************/ + +static void test_powerloss_dirs(void) +{ + const int max_ops = 12; + const size_t len = 2000; + struct stat st; + bool all_ok = true; + bool saw_failure = false; + bool saw_created = false; + bool saw_absent = false; + char detail[80]; + int quiet = 0; + int ret; + int n; + + printf("\n-- power loss during mkdir and rmdir --\n"); + detail[0] = '\0'; + + for (n = 0; n < max_ops; n++) + { + /* Baseline: a directory holding a file, plus a file beside it, so a + * torn commit has neighbours to damage and a child to orphan. + */ + + ret = remount(); + if (ret < 0) + { + snprintf(detail, sizeof(detail), + "remount failed before injection %d: %d", n, ret); + all_ok = false; + break; + } + + unlink(PATH("d_keep/inner.bin")); + rmdir(PATH("d_keep")); + rmdir(PATH("d_new")); + unlink(PATH("d_file.bin")); + + if (mkdir(PATH("d_keep"), 0755) < 0 || + create_file(PATH("d_keep/inner.bin"), len, 0xd1) < 0 || + create_file(PATH("d_file.bin"), len, 0xd2) < 0) + { + snprintf(detail, sizeof(detail), "baseline failed at %d", n); + all_ok = false; + break; + } + + if (arm_injector(n, XIPFS_FAULT_TORN) < 0) + { + strlcpy(detail, "cannot arm injector", sizeof(detail)); + all_ok = false; + break; + } + + /* The operation being cut short */ + + if (mkdir(PATH("d_new"), 0755) < 0) + { + saw_failure = true; + quiet = 0; + } + else if (++quiet >= 2) + { + break; /* Walked off the end of the mkdir */ + } + + ret = remount(); + if (ret < 0) + { + snprintf(detail, sizeof(detail), + "mount failed after injection %d: %d", n, ret); + all_ok = false; + break; + } + + /* The new directory is either there or not, and either way the tree + * that was already committed is untouched and still walkable. + */ + + if (stat(PATH("d_new"), &st) == 0) + { + saw_created = true; + + if (!S_ISDIR(st.st_mode)) + { + snprintf(detail, sizeof(detail), + "mkdir left a non-directory at injection %d", n); + all_ok = false; + break; + } + } + else + { + saw_absent = true; + } + + if (stat(PATH("d_keep"), &st) != 0 || !S_ISDIR(st.st_mode) || + !verify_file(PATH("d_keep/inner.bin"), len, 0xd1) || + !verify_file(PATH("d_file.bin"), len, 0xd2)) + { + snprintf(detail, sizeof(detail), + "committed tree damaged by injection %d", n); + all_ok = false; + break; + } + + /* And the same for a removal cut short. The directory must not come + * back empty-but-present while its child is gone, nor vice versa. + */ + + if (arm_injector(n, XIPFS_FAULT_TORN) < 0) + { + strlcpy(detail, "cannot arm injector", sizeof(detail)); + all_ok = false; + break; + } + + rmdir(PATH("d_new")); + + ret = remount(); + if (ret < 0) + { + snprintf(detail, sizeof(detail), + "mount failed after rmdir injection %d: %d", n, ret); + all_ok = false; + break; + } + + if (stat(PATH("d_keep"), &st) != 0 || + !verify_file(PATH("d_keep/inner.bin"), len, 0xd1)) + { + snprintf(detail, sizeof(detail), + "rmdir injection %d damaged another directory", n); + all_ok = false; + break; + } + } + + arm_injector(-1, XIPFS_FAULT_CLEAN); + + CHECK("mkdir and rmdir stay consistent across every injection point", + all_ok, detail); + CHECK("injection actually interrupted some directory operations", + saw_failure, "nothing was ever cut short"); + CHECK("both outcomes observed across the sweep", + saw_created && saw_absent, "only one outcome seen"); + + unlink(PATH("d_keep/inner.bin")); + rmdir(PATH("d_keep")); + rmdir(PATH("d_new")); + unlink(PATH("d_file.bin")); +} + +/**************************************************************************** + * Name: want + * + * Description: + * Whether a named section should run. With no argument everything does. + * + * The selector exists because the power loss sweeps dominate the runtime + * -- they are minutes, everything else is seconds -- so iterating on any + * other section means waiting for work that is not being changed. + * + ****************************************************************************/ + +static bool want(FAR const char *name, int argc, FAR char *argv[]) +{ + return argc < 2 || strcmp(argv[1], name) == 0; +} + +#define SECTION(name, fn) \ + do \ + { \ + if (want((name), argc, argv)) \ + { \ + fn(); \ + } \ + } \ + while (0) + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +int main(int argc, FAR char *argv[]) +{ + printf("XIPFS test suite on %s (%s)\n", MOUNTPT, MTDDEV); + + /* In a flat build the counters keep their values from the previous run, + * so a second invocation would report the first one's results too. + */ + + g_passed = 0; + g_failed = 0; + + g_buffer = malloc(g_bufsize); + if (g_buffer == NULL) + { + printf("ERROR: out of memory\n"); + return EXIT_FAILURE; + } + + if (remount() < 0) + { + printf("ERROR: cannot mount %s; is the board bringup configured?\n", + MOUNTPT); + free(g_buffer); + return EXIT_FAILURE; + } + + SECTION("basic", test_basic); + SECTION("writeonce", test_writeonce); + SECTION("strict", test_strict); + SECTION("xip", test_xip); + SECTION("multimap", test_multimap); + SECTION("crosstask", test_crosstask_pins); + SECTION("teardown", test_teardown_release); + SECTION("dirs", test_dirs); + SECTION("defrag", test_defrag); + SECTION("powerloss", test_powerloss); + SECTION("powertorn", test_powerloss_torn); + SECTION("unlink", test_powerloss_unlink); + SECTION("dirloss", test_powerloss_dirs); + SECTION("defraglos", test_powerloss_defrag); + + if (g_passed == 0 && g_failed == 0) + { + printf("\nno section matched '%s'\n", argv[1]); + free(g_buffer); + return EXIT_FAILURE; + } + + printf("\n==== %d passed, %d failed ====\n", g_passed, g_failed); + + free(g_buffer); + return g_failed == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +}