configfs_rmdir() drops the last reference to an item while its dentry is still hashed, so a symlink(2) resolving that target takes a reference on freed memory. syzbot reported it [1]. Race mkdir/rmdir of a target against symlink/unlink of a link to it, watching /sys/kernel/warn_count. Fails until the fix [2] lands. Link: https://syzkaller.appspot.com/bug?extid=6b16e3d085833cbf3e25 [1] Link: https://lore.kernel.org/all/20260730093435.195441-1-vasilisalmpanis@gmail.com/ [2] Signed-off-by: Breno Leitao --- .../selftests/filesystems/configfs/Makefile | 2 +- .../selftests/filesystems/configfs/configfs_test.c | 62 ++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/tools/testing/selftests/filesystems/configfs/Makefile b/tools/testing/selftests/filesystems/configfs/Makefile index 359296356c831..40a91ed788ed2 100644 --- a/tools/testing/selftests/filesystems/configfs/Makefile +++ b/tools/testing/selftests/filesystems/configfs/Makefile @@ -2,7 +2,7 @@ # Copyright (c) 2026 Meta Platforms, Inc. and affiliates # Copyright (c) 2026 Breno Leitao -CFLAGS += -Wall -Werror +CFLAGS += -Wall -Werror -pthread TEST_GEN_PROGS := configfs_test include ../../lib.mk diff --git a/tools/testing/selftests/filesystems/configfs/configfs_test.c b/tools/testing/selftests/filesystems/configfs/configfs_test.c index 9b15e1fd69e5b..c6a1049e5852e 100644 --- a/tools/testing/selftests/filesystems/configfs/configfs_test.c +++ b/tools/testing/selftests/filesystems/configfs/configfs_test.c @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,8 @@ #define LINK_SRC SYMLINKS "/kselftest-src" #define LINK LINK_SRC "/kselftest-link" +#define RACE_ITERATIONS 20000 + static const char * const test_links[] = { LINK, }; @@ -405,6 +408,65 @@ TEST_F(configfs, symlink_target_is_an_attribute) EXPECT_EQ(errno, ENOTDIR); } +static volatile int race_stop; + +static void *rmdir_target(void *arg) +{ + while (!race_stop) { + if (mkdir(ITEM_A, 0755) == 0 || errno == EEXIST) + rmdir(ITEM_A); + } + + return NULL; +} + +/* -1 if the kernel does not export a warning counter. */ +static long warn_count(void) +{ + char buf[32]; + + if (read_attr("/sys/kernel/warn_count", buf, sizeof(buf)) < 0) + return -1; + + return strtol(buf, NULL, 10); +} + +TEST_F(configfs, symlink_races_with_target_rmdir) +{ + pthread_t thread; + long warns; + int i; + + warns = warn_count(); + if (warns < 0) + SKIP(return, "no /sys/kernel/warn_count to watch"); + + ASSERT_EQ(mkdir(LINK_SRC, 0755), 0); + ASSERT_EQ(pthread_create(&thread, NULL, rmdir_target, NULL), 0); + + /* + * configfs_rmdir() drops the last reference to the item while its + * dentry is still hashed, and get_target() takes a hashed dentry as + * proof that the item behind it is alive. The symlink then walks + * ->ci_dentry into a released dirent, which configfs_get() warns + * about. KASAN sees the freed item itself. + */ + for (i = 0; i < RACE_ITERATIONS; i++) { + if (symlink(ITEM_A, LINK) == 0) + unlink(LINK); + + /* Give up on the first splat rather than flood the log. */ + if (!(i % 128) && warn_count() != warns) + break; + } + + race_stop = 1; + ASSERT_EQ(pthread_join(thread, NULL), 0); + + EXPECT_EQ(warn_count(), warns) + TH_LOG("kernel warned after %d iterations", i); +} + TEST_F(configfs, module_pinned_by_item) { ASSERT_EQ(mkdir(ITEM_A, 0755), 0); -- 2.53.0-Meta