configfs has no selftest coverage. Its userspace ABI is entirely mkdir/rmdir/read/write on attribute files, and the error each of those returns is part of the contract. Add tests driving the subsystems samples/configfs registers, each in its own mount namespace on a private configfs mount. Signed-off-by: Breno Leitao --- MAINTAINERS | 1 + tools/testing/selftests/Makefile | 1 + .../selftests/filesystems/configfs/.gitignore | 2 + .../selftests/filesystems/configfs/Makefile | 8 + .../testing/selftests/filesystems/configfs/config | 5 + .../selftests/filesystems/configfs/configfs_test.c | 338 +++++++++++++++++++++ 6 files changed, 355 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index fe10d29b7657b..69aea9931b774 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -6604,6 +6604,7 @@ S: Supported F: fs/configfs/ F: include/linux/configfs.h F: samples/configfs/ +F: tools/testing/selftests/filesystems/configfs/ CONFIGFS [RUST] M: Andreas Hindborg diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile index 2d960626750e3..b328a235f1295 100644 --- a/tools/testing/selftests/Makefile +++ b/tools/testing/selftests/Makefile @@ -34,6 +34,7 @@ TARGETS += exec TARGETS += fchmodat2 TARGETS += filesystems TARGETS += filesystems/binderfs +TARGETS += filesystems/configfs TARGETS += filesystems/epoll TARGETS += filesystems/failfs TARGETS += filesystems/fat diff --git a/tools/testing/selftests/filesystems/configfs/.gitignore b/tools/testing/selftests/filesystems/configfs/.gitignore new file mode 100644 index 0000000000000..accfb6bb48268 --- /dev/null +++ b/tools/testing/selftests/filesystems/configfs/.gitignore @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-only +configfs_test diff --git a/tools/testing/selftests/filesystems/configfs/Makefile b/tools/testing/selftests/filesystems/configfs/Makefile new file mode 100644 index 0000000000000..359296356c831 --- /dev/null +++ b/tools/testing/selftests/filesystems/configfs/Makefile @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0 +# Copyright (c) 2026 Meta Platforms, Inc. and affiliates +# Copyright (c) 2026 Breno Leitao + +CFLAGS += -Wall -Werror +TEST_GEN_PROGS := configfs_test + +include ../../lib.mk diff --git a/tools/testing/selftests/filesystems/configfs/config b/tools/testing/selftests/filesystems/configfs/config new file mode 100644 index 0000000000000..5ea17df535b3f --- /dev/null +++ b/tools/testing/selftests/filesystems/configfs/config @@ -0,0 +1,5 @@ +CONFIG_CONFIGFS_FS=y +CONFIG_MODULES=y +CONFIG_MODULE_UNLOAD=y +CONFIG_SAMPLES=y +CONFIG_SAMPLE_CONFIGFS=m diff --git a/tools/testing/selftests/filesystems/configfs/configfs_test.c b/tools/testing/selftests/filesystems/configfs/configfs_test.c new file mode 100644 index 0000000000000..072d0dcf3f96b --- /dev/null +++ b/tools/testing/selftests/filesystems/configfs/configfs_test.c @@ -0,0 +1,338 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Exercise the configfs userspace interface through the three subsystems + * registered by samples/configfs. + * + * Copyright (c) 2026 Meta Platforms, Inc. and affiliates + * Copyright (c) 2026 Breno Leitao + */ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "kselftest_harness.h" + +/* Private to fs/configfs/mount.c. */ +#define CONFIGFS_MAGIC 0x62656570 + +#define SAMPLE_MODULE "configfs_sample" + +#define CHILDLESS "01-childless" +#define SIMPLE "02-simple-children" +#define GROUPS "03-group-children" + +#define ITEM_A SIMPLE "/kselftest-a" +#define ITEM_B SIMPLE "/kselftest-b" +#define GROUP GROUPS "/kselftest-group" +#define GROUP_ITEM GROUP "/kselftest-a" + +/* Deepest first, so one pass empties the tree. */ +static const char * const test_dirs[] = { + GROUP_ITEM, + GROUP, + ITEM_A, + ITEM_B, +}; + +static void drop_test_dirs(void) +{ + size_t i; + + for (i = 0; i < ARRAY_SIZE(test_dirs); i++) + rmdir(test_dirs[i]); +} + +static ssize_t read_attr(const char *path, char *buf, size_t len) +{ + ssize_t ret; + int fd; + + fd = open(path, O_RDONLY); + if (fd < 0) + return -1; + + ret = read(fd, buf, len - 1); + close(fd); + if (ret < 0) + return -1; + + buf[ret] = '\0'; + return ret; +} + +static ssize_t write_attr(const char *path, const char *val) +{ + ssize_t ret; + int fd, err; + + fd = open(path, O_WRONLY); + if (fd < 0) + return -1; + + ret = write(fd, val, strlen(val)); + err = errno; + close(fd); + errno = err; + + return ret; +} + +FIXTURE(configfs) { + char mnt[sizeof(P_tmpdir "/configfs_XXXXXX")]; + bool mounted; +}; + +FIXTURE_SETUP(configfs) +{ + char tmpl[] = P_tmpdir "/configfs_XXXXXX"; + + if (geteuid()) + SKIP(return, "need root to load modules and mount configfs"); + + ASSERT_EQ(system("modprobe -q " SAMPLE_MODULE), 0) + TH_LOG(SAMPLE_MODULE " missing, is CONFIG_SAMPLE_CONFIGFS=m?"); + + ASSERT_EQ(unshare(CLONE_NEWNS), 0); + ASSERT_EQ(mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL), 0); + + ASSERT_NE(mkdtemp(tmpl), NULL); + strcpy(self->mnt, tmpl); + + ASSERT_EQ(mount("configfs", self->mnt, "configfs", 0, NULL), 0); + ASSERT_EQ(chdir(self->mnt), 0); + self->mounted = true; + + /* configfs items outlive the mount, so a killed run leaves some. */ + drop_test_dirs(); +} + +FIXTURE_TEARDOWN(configfs) +{ + if (self->mounted) { + drop_test_dirs(); + EXPECT_EQ(chdir("/"), 0); + EXPECT_EQ(umount2(self->mnt, MNT_DETACH), 0); + } + + if (self->mnt[0]) + EXPECT_EQ(rmdir(self->mnt), 0); +} + +TEST_F(configfs, mount_and_subsystems) +{ + const char * const subsys[] = { CHILDLESS, SIMPLE, GROUPS }; + struct statfs sfs; + struct stat st; + size_t i; + + ASSERT_EQ(statfs(".", &sfs), 0); + EXPECT_EQ(sfs.f_type, CONFIGFS_MAGIC); + + for (i = 0; i < ARRAY_SIZE(subsys); i++) { + ASSERT_EQ(stat(subsys[i], &st), 0) + TH_LOG("%s is missing", subsys[i]); + EXPECT_TRUE(S_ISDIR(st.st_mode)); + } +} + +TEST_F(configfs, mkdir_at_root) +{ + /* The root has no ->mkdir(); only subsystems register there. */ + ASSERT_EQ(mkdir("kselftest-root", 0755), -1); + EXPECT_EQ(errno, EPERM); +} + +TEST_F(configfs, rmdir_subsystem) +{ + ASSERT_EQ(rmdir(CHILDLESS), -1); + EXPECT_EQ(errno, EPERM); +} + +TEST_F(configfs, mkdir_without_group_ops) +{ + /* 01-childless has attributes but no ->make_item()/->make_group(). */ + ASSERT_EQ(mkdir(CHILDLESS "/kselftest-a", 0755), -1); + EXPECT_EQ(errno, EPERM); +} + +TEST_F(configfs, attr_store_and_show) +{ + char buf[64]; + + ASSERT_GT(write_attr(CHILDLESS "/storeme", "42"), 0); + ASSERT_GT(read_attr(CHILDLESS "/storeme", buf, sizeof(buf)), 0); + EXPECT_STREQ(buf, "42\n"); +} + +TEST_F(configfs, attr_store_rejects_garbage) +{ + ASSERT_EQ(write_attr(CHILDLESS "/storeme", "not-a-number"), -1); + EXPECT_EQ(errno, EINVAL); +} + +TEST_F(configfs, attr_show_runs_on_every_open) +{ + char first[64], second[64]; + + /* 01-childless/showme increments the value it just returned. */ + ASSERT_GT(read_attr(CHILDLESS "/showme", first, sizeof(first)), 0); + ASSERT_GT(read_attr(CHILDLESS "/showme", second, sizeof(second)), 0); + EXPECT_EQ(atoi(second), atoi(first) + 1); +} + +TEST_F(configfs, attr_read_only) +{ + ASSERT_EQ(open(CHILDLESS "/description", O_WRONLY), -1); + EXPECT_EQ(errno, EACCES); +} + +TEST_F(configfs, attr_unlink) +{ + /* ->unlink() only accepts the symlinks configfs itself created. */ + ASSERT_EQ(unlink(CHILDLESS "/storeme"), -1); + EXPECT_EQ(errno, EPERM); +} + +TEST_F(configfs, attr_read_length) +{ + char buf[8192]; + struct stat st; + ssize_t n; + int fd; + + fd = open(CHILDLESS "/description", O_RDONLY); + ASSERT_GE(fd, 0); + + /* Attributes report a page, whatever ->show() ends up producing. */ + ASSERT_EQ(fstat(fd, &st), 0); + EXPECT_EQ(st.st_size, sysconf(_SC_PAGESIZE)); + + n = read(fd, buf, sizeof(buf)); + ASSERT_GT(n, 0); + EXPECT_LT(n, st.st_size); + EXPECT_EQ(read(fd, buf, sizeof(buf)), 0); + + EXPECT_EQ(close(fd), 0); +} + +TEST_F(configfs, attr_write_is_not_incremental) +{ + char buf[64]; + int fd; + + /* + * Every write hands the whole buffer to ->store() and the file + * position is ignored, so the second write replaces the first. + */ + fd = open(CHILDLESS "/storeme", O_WRONLY); + ASSERT_GE(fd, 0); + ASSERT_EQ(write(fd, "1", 1), 1); + ASSERT_EQ(write(fd, "2", 1), 1); + EXPECT_EQ(close(fd), 0); + + ASSERT_GT(read_attr(CHILDLESS "/storeme", buf, sizeof(buf)), 0); + EXPECT_STREQ(buf, "2\n"); +} + +TEST_F(configfs, item_create_and_drop) +{ + struct stat st; + + ASSERT_EQ(mkdir(ITEM_A, 0755), 0); + EXPECT_EQ(stat(ITEM_A "/storeme", &st), 0); + + /* The item carries its own attributes, not the subsystem's. */ + ASSERT_EQ(stat(ITEM_A "/description", &st), -1); + EXPECT_EQ(errno, ENOENT); + + ASSERT_EQ(rmdir(ITEM_A), 0); + ASSERT_EQ(stat(ITEM_A, &st), -1); + EXPECT_EQ(errno, ENOENT); +} + +TEST_F(configfs, item_create_twice) +{ + ASSERT_EQ(mkdir(ITEM_A, 0755), 0); + ASSERT_EQ(mkdir(ITEM_A, 0755), -1); + EXPECT_EQ(errno, EEXIST); +} + +TEST_F(configfs, item_has_no_children) +{ + /* ->make_item() produces an item, so it cannot nest. */ + ASSERT_EQ(mkdir(ITEM_A, 0755), 0); + ASSERT_EQ(mkdir(ITEM_A "/kselftest-b", 0755), -1); + EXPECT_EQ(errno, EPERM); +} + +TEST_F(configfs, item_attrs_are_private) +{ + char buf[64]; + + ASSERT_EQ(mkdir(ITEM_A, 0755), 0); + ASSERT_EQ(mkdir(ITEM_B, 0755), 0); + + ASSERT_GT(write_attr(ITEM_A "/storeme", "11"), 0); + ASSERT_GT(write_attr(ITEM_B "/storeme", "22"), 0); + + ASSERT_GT(read_attr(ITEM_A "/storeme", buf, sizeof(buf)), 0); + EXPECT_STREQ(buf, "11\n"); + ASSERT_GT(read_attr(ITEM_B "/storeme", buf, sizeof(buf)), 0); + EXPECT_STREQ(buf, "22\n"); +} + +TEST_F(configfs, group_create_and_drop) +{ + struct stat st; + + /* 03-group-children hands out groups that take items of their own. */ + ASSERT_EQ(mkdir(GROUP, 0755), 0); + EXPECT_EQ(stat(GROUP "/description", &st), 0); + + ASSERT_EQ(mkdir(GROUP_ITEM, 0755), 0); + EXPECT_EQ(stat(GROUP_ITEM "/storeme", &st), 0); + + ASSERT_EQ(rmdir(GROUP), -1); + EXPECT_EQ(errno, ENOTEMPTY); + + ASSERT_EQ(rmdir(GROUP_ITEM), 0); + ASSERT_EQ(rmdir(GROUP), 0); +} + +TEST_F(configfs, rename_item) +{ + ASSERT_EQ(mkdir(ITEM_A, 0755), 0); + ASSERT_EQ(rename(ITEM_A, ITEM_B), -1); + EXPECT_EQ(errno, EPERM); +} + +TEST_F(configfs, symlink_without_allow_link) +{ + ASSERT_EQ(mkdir(ITEM_A, 0755), 0); + ASSERT_EQ(symlink(ITEM_A, SIMPLE "/kselftest-link"), -1); + EXPECT_EQ(errno, EPERM); +} + +TEST_F(configfs, module_pinned_by_item) +{ + ASSERT_EQ(mkdir(ITEM_A, 0755), 0); + + /* mkdir() pins both the subsystem's module and the new item's. */ + ASSERT_EQ(syscall(__NR_delete_module, SAMPLE_MODULE, O_NONBLOCK), -1); + if (errno == ENOSYS) + SKIP(return, "kernel built without CONFIG_MODULE_UNLOAD"); + EXPECT_EQ(errno, EWOULDBLOCK); +} + +TEST_HARNESS_MAIN -- 2.53.0-Meta