diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist index 84bd2f09e7cc..92a0008d2a0c 100644 --- a/etc/mtree/BSD.tests.dist +++ b/etc/mtree/BSD.tests.dist @@ -838,6 +838,8 @@ .. tmpfs .. + ufs + .. unionfs .. .. diff --git a/tests/sys/fs/Makefile b/tests/sys/fs/Makefile index 842ec161ca07..7c8194bb5f53 100644 --- a/tests/sys/fs/Makefile +++ b/tests/sys/fs/Makefile @@ -16,6 +16,7 @@ TESTS_SUBDIRS+= fusefs TESTS_SUBDIRS+= pjdfstest TESTS_SUBDIRS+= tarfs TESTS_SUBDIRS+= tmpfs +TESTS_SUBDIRS+= ufs TESTS_SUBDIRS+= unionfs ${PACKAGE}FILES+= h_funcs.subr diff --git a/tests/sys/fs/ufs/Makefile b/tests/sys/fs/ufs/Makefile new file mode 100644 index 000000000000..034cf5429d9a --- /dev/null +++ b/tests/sys/fs/ufs/Makefile @@ -0,0 +1,9 @@ +PACKAGE= tests + +TESTSDIR= ${TESTSBASE}/sys/fs/ufs + +ATF_TESTS_C+= sync_test + +LIBADD+= ufs + +.include diff --git a/tests/sys/fs/ufs/sync_test.c b/tests/sys/fs/ufs/sync_test.c new file mode 100644 index 000000000000..9adc853affd7 --- /dev/null +++ b/tests/sys/fs/ufs/sync_test.c @@ -0,0 +1,386 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2026 Maksym Sobolyev + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/* + * Once fsync(2) or fdatasync(2) returns, or a write(2) to a descriptor + * opened with O_SYNC or O_DSYNC returns, the data written must be + * retrievable after a crash. When the write extended the file or + * allocated a block, that includes the new file size and the block + * pointers leading to the new block, whether they are in the inode or in + * an indirect block. + * + * Each test runs on an md(4)-backed UFS and, right after the synchronous + * operation returns, reads the metadata and the data straight from the + * device with libufs. There is no buffer cache in front of the disk + * device, so this shows exactly what would survive a crash at that + * moment. Nothing else writes the file's metadata within the test's + * lifetime: the syncer only gets to it after kern.filedelay (30s by + * default). + */ + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MNT "mnt" +#define MDFILE "md.unit" +#define TFILE MNT "/file" +#define PATTERN 0xa5 + +enum sync_method { + M_FSYNC, + M_FDATASYNC, + M_OSYNC, + M_ODSYNC, +}; + +/* + * The file is prepared with "prefill" blocks of data (or "prefill" blocks + * of hole when "sparse"), synced with fsync(), and then block "lbn" is + * written with one of the synchronous methods. If "unsynced" is not -1, + * that block is written first without syncing, as another descriptor of a + * process that does not care about durability would. With O_SYNC and + * O_DSYNC on soft updates, the open that follows must already have made it + * durable. + */ +struct scenario { + int prefill; + bool sparse; + int lbn; + int unsynced; +}; + +static const struct scenario s_extend = { 1, false, 1, -1 }; +static const struct scenario s_fill_hole = { 4, true, 2, -1 }; +/* First block past the direct ones: allocates the indirect block too. */ +static const struct scenario s_extend_newindir = + { UFS_NDADDR, false, UFS_NDADDR, -1 }; +/* The indirect block exists, only a pointer in it is added. */ +static const struct scenario s_extend_indir = + { UFS_NDADDR + 1, false, UFS_NDADDR + 1, -1 }; +/* + * With soft updates, an inode written while the allocation of a direct + * block is incomplete has its size cut back to that block and all the + * pointers after it cleared, those to the indirect blocks included. + */ +static const struct scenario s_extend_unsynced = { 2, false, 4, 2 }; +static const struct scenario s_extend_newindir_unsynced = + { 2, false, UFS_NDADDR, 2 }; + +static char mddev[64]; + +static void +xsystem(const char *fmt, ...) +{ + char cmd[256]; + va_list ap; + int rv; + + va_start(ap, fmt); + vsnprintf(cmd, sizeof(cmd), fmt, ap); + va_end(ap); + rv = system(cmd); + ATF_REQUIRE_MSG(rv == 0, "\"%s\" failed: %d", cmd, rv); +} + +/* + * Create a swap-backed md(4), newfs it with the given options and mount it. + * NULL options mean a file system without soft updates, which newfs(8) + * enables by default. The md unit is recorded in MDFILE so that cleanup + * can find it. + */ +static void +ufs_setup(const char *newfs_opts) +{ + struct statfs sfs; + FILE *fp; + size_t len; + + fp = popen("mdconfig -a -t swap -s 64m", "r"); + ATF_REQUIRE(fp != NULL); + ATF_REQUIRE(fgets(mddev, sizeof(mddev), fp) != NULL); + ATF_REQUIRE_EQ(0, pclose(fp)); + len = strcspn(mddev, "\n"); + mddev[len] = '\0'; + ATF_REQUIRE(len > 0); + + fp = fopen(MDFILE, "w"); + ATF_REQUIRE(fp != NULL); + fprintf(fp, "%s\n", mddev); + ATF_REQUIRE_EQ(0, fclose(fp)); + + if (newfs_opts != NULL) { + xsystem("newfs %s /dev/%s >/dev/null", newfs_opts, mddev); + } else { + xsystem("newfs /dev/%s >/dev/null", mddev); + xsystem("tunefs -n disable /dev/%s >/dev/null", mddev); + } + ATF_REQUIRE_EQ(0, mkdir(MNT, 0755)); + xsystem("mount /dev/%s %s", mddev, MNT); + ATF_REQUIRE_EQ(0, statfs(MNT, &sfs)); + ATF_REQUIRE_EQ_MSG(newfs_opts != NULL, + (sfs.f_flags & MNT_SOFTDEP) != 0, + "unexpected soft updates state, mount flags %#jx", + (uintmax_t)sfs.f_flags); +} + +static void +ufs_cleanup(void) +{ + char cmd[128], unit[64]; + FILE *fp; + + (void)unmount(MNT, MNT_FORCE); + fp = fopen(MDFILE, "r"); + if (fp == NULL) + return; + if (fgets(unit, sizeof(unit), fp) != NULL) { + unit[strcspn(unit, "\n")] = '\0'; + snprintf(cmd, sizeof(cmd), "mdconfig -d -u %s", unit); + (void)system(cmd); + } + fclose(fp); +} + +struct ondisk { + off_t size; + ufs2_daddr_t blkno; /* 0 if not allocated on disk */ + bool data_ok; /* block contents are PATTERN */ +}; + +/* + * Read inode "ino" from the device and follow its block pointers to + * logical block "lbn" (a direct block or one in the single indirect block). + */ +static void +ondisk_read(ino_t ino, int lbn, struct ondisk *od) +{ + char dev[80]; + struct uufsd disk; + union dinodep dp; + struct fs *fs; + ufs2_daddr_t ib; + uint8_t *buf; + long i; + + ATF_REQUIRE(lbn < UFS_NDADDR + 1024); + snprintf(dev, sizeof(dev), "/dev/%s", mddev); + ATF_REQUIRE_MSG(ufs_disk_fillout(&disk, dev) == 0, + "ufs_disk_fillout(%s): %s", dev, disk.d_error); + fs = &disk.d_fs; + ATF_REQUIRE_MSG(getinode(&disk, &dp, ino) == 0, + "getinode(%ju): %s", (uintmax_t)ino, disk.d_error); + buf = malloc(fs->fs_bsize); + ATF_REQUIRE(buf != NULL); + + od->size = disk.d_ufs == 1 ? dp.dp1->di_size : dp.dp2->di_size; + if (lbn < UFS_NDADDR) { + od->blkno = disk.d_ufs == 1 ? dp.dp1->di_db[lbn] : + dp.dp2->di_db[lbn]; + } else { + ib = disk.d_ufs == 1 ? dp.dp1->di_ib[0] : dp.dp2->di_ib[0]; + od->blkno = 0; + if (ib != 0) { + ATF_REQUIRE(bread(&disk, fsbtodb(fs, ib), buf, + fs->fs_bsize) == fs->fs_bsize); + od->blkno = disk.d_ufs == 1 ? + ((ufs1_daddr_t *)buf)[lbn - UFS_NDADDR] : + ((ufs2_daddr_t *)buf)[lbn - UFS_NDADDR]; + } + } + + od->data_ok = false; + if (od->blkno != 0) { + ATF_REQUIRE(bread(&disk, fsbtodb(fs, od->blkno), buf, + fs->fs_bsize) == fs->fs_bsize); + for (i = 0; i < fs->fs_bsize && buf[i] == PATTERN; i++) + continue; + od->data_ok = i == fs->fs_bsize; + } + free(buf); + ufs_disk_close(&disk); +} + +static void +fill(int fd, off_t off, size_t len) +{ + char *buf; + + buf = malloc(len); + ATF_REQUIRE(buf != NULL); + memset(buf, PATTERN, len); + ATF_REQUIRE_EQ((ssize_t)len, pwrite(fd, buf, len, off)); + free(buf); +} + +static void +test_sync(const char *newfs_opts, const struct scenario *sc, + enum sync_method method) +{ + struct ondisk od; + struct stat sb; + off_t bsize, expsize; + int fd, i, oflags; + + ufs_setup(newfs_opts); + fd = open(TFILE, O_RDWR | O_CREAT | O_TRUNC, 0644); + ATF_REQUIRE(fd >= 0); + ATF_REQUIRE_EQ(0, fstat(fd, &sb)); + bsize = sb.st_blksize; + if (sc->sparse) { + ATF_REQUIRE_EQ(0, ftruncate(fd, sc->prefill * bsize)); + } else { + for (i = 0; i < sc->prefill; i++) + fill(fd, i * bsize, bsize); + } + ATF_REQUIRE_EQ(0, fsync(fd)); + expsize = MAX(MAX(sc->prefill, sc->lbn + 1), sc->unsynced + 1) * bsize; + + /* Sanity check the starting point. */ + ondisk_read(sb.st_ino, sc->lbn, &od); + ATF_REQUIRE_EQ(sc->prefill * bsize, od.size); + ATF_REQUIRE_EQ(0, od.blkno); + + if (sc->unsynced != -1) + fill(fd, sc->unsynced * bsize, bsize); + + switch (method) { + case M_FSYNC: + case M_FDATASYNC: + fill(fd, sc->lbn * bsize, bsize); + if (method == M_FSYNC) + ATF_REQUIRE_EQ(0, fsync(fd)); + else + ATF_REQUIRE_EQ(0, fdatasync(fd)); + break; + case M_OSYNC: + case M_ODSYNC: + oflags = method == M_OSYNC ? O_SYNC : O_DSYNC; + ATF_REQUIRE_EQ(0, close(fd)); + fd = open(TFILE, O_RDWR | oflags); + ATF_REQUIRE(fd >= 0); + if (sc->unsynced != -1 && newfs_opts != NULL) { + /* + * With soft updates, the open syncs what was + * written before it. + */ + ondisk_read(sb.st_ino, sc->unsynced, &od); + ATF_CHECK_MSG(od.size >= (sc->unsynced + 1) * bsize, + "on-disk size is %jd after open", + (intmax_t)od.size); + ATF_CHECK_MSG(od.blkno != 0 && od.data_ok, + "unsynced block %d is not on disk after open", + sc->unsynced); + } + fill(fd, sc->lbn * bsize, bsize); + break; + } + + ondisk_read(sb.st_ino, sc->lbn, &od); + ATF_CHECK_EQ_MSG(expsize, od.size, + "on-disk size is %jd, expected %jd", (intmax_t)od.size, + (intmax_t)expsize); + ATF_CHECK_MSG(od.blkno != 0, + "on-disk pointer to block %d is 0", sc->lbn); + ATF_CHECK_MSG(od.blkno == 0 || od.data_ok, + "on-disk block %d does not hold the data written", sc->lbn); + ATF_REQUIRE_EQ(0, close(fd)); +} + +#define UFS_TC(name, opts, sc, method) \ +ATF_TC_WITH_CLEANUP(name); \ +ATF_TC_HEAD(name, tc) \ +{ \ + atf_tc_set_md_var(tc, "descr", "Write " #sc " with " #method \ + ", newfs options " #opts); \ + atf_tc_set_md_var(tc, "require.user", "root"); \ + atf_tc_set_md_var(tc, "require.progs", \ + "mdconfig newfs tunefs mount"); \ +} \ +ATF_TC_BODY(name, tc) \ +{ \ + test_sync(opts, &sc, method); \ +} \ +ATF_TC_CLEANUP(name, tc) \ +{ \ + ufs_cleanup(); \ +} + +#define UFS_TCS_FS(prefix, sc, method) \ + UFS_TC(prefix ## _nosu, NULL, sc, method) \ + UFS_TC(prefix ## _su, "-U", sc, method) \ + UFS_TC(prefix ## _suj, "-j", sc, method) + +#define UFS_TCS(prefix, sc) \ + UFS_TCS_FS(prefix ## _fsync, sc, M_FSYNC) \ + UFS_TCS_FS(prefix ## _fdatasync, sc, M_FDATASYNC) \ + UFS_TCS_FS(prefix ## _osync, sc, M_OSYNC) \ + UFS_TCS_FS(prefix ## _odsync, sc, M_ODSYNC) + +UFS_TCS(extend, s_extend) +UFS_TCS(fill_hole, s_fill_hole) +UFS_TCS(extend_newindir, s_extend_newindir) +UFS_TCS(extend_indir, s_extend_indir) +UFS_TCS(extend_unsynced, s_extend_unsynced) +UFS_TCS(extend_newindir_unsynced, s_extend_newindir_unsynced) + +#define UFS_TCS_ADD_FS(tp, prefix) \ + ATF_TP_ADD_TC(tp, prefix ## _nosu); \ + ATF_TP_ADD_TC(tp, prefix ## _su); \ + ATF_TP_ADD_TC(tp, prefix ## _suj) + +#define UFS_TCS_ADD(tp, prefix) \ + UFS_TCS_ADD_FS(tp, prefix ## _fsync); \ + UFS_TCS_ADD_FS(tp, prefix ## _fdatasync); \ + UFS_TCS_ADD_FS(tp, prefix ## _osync); \ + UFS_TCS_ADD_FS(tp, prefix ## _odsync) + +ATF_TP_ADD_TCS(tp) +{ + UFS_TCS_ADD(tp, extend); + UFS_TCS_ADD(tp, fill_hole); + UFS_TCS_ADD(tp, extend_newindir); + UFS_TCS_ADD(tp, extend_indir); + UFS_TCS_ADD(tp, extend_unsynced); + UFS_TCS_ADD(tp, extend_newindir_unsynced); + return (atf_no_error()); +}