From 17ca0bc06bb246d9488556f3f16ad6f37a4fc5ad Mon Sep 17 00:00:00 2001 From: Yilin Chen <1479826151@qq.com> Date: Wed, 8 Jul 2026 07:21:06 +0000 Subject: [PATCH 01/13] rust: configfs: fix release safety documentation ItemOperationsVTable, Data>::release() receives a pointer to a config_item, not a config_group. The old Safety docs described the pointer as a config_group embedded in Group. Update them to state that the pointer must be the cg_item field of a config_group embedded in a valid Group. Assisted-by: Codex:GPT-5 Signed-off-by: Yilin Chen <1479826151@qq.com> Link: https://msgid.link/tencent_9FAFC60382D724C608F0F6BFABFC63BB6707@qq.com Signed-off-by: Andreas Hindborg --- rust/kernel/configfs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/kernel/configfs.rs b/rust/kernel/configfs.rs index cd082b83e9e7..7722035ebc02 100644 --- a/rust/kernel/configfs.rs +++ b/rust/kernel/configfs.rs @@ -437,8 +437,8 @@ impl ItemOperationsVTable, Data> { /// # Safety /// - /// `this` must be a pointer to a `bindings::config_group` embedded in a - /// `Group`. + /// `this` must be a pointer to the `cg_item` field of a `bindings::config_group` embedded in a + /// valid `Group`. /// /// This function will destroy the pointee of `this`. The pointee of `this` /// must not be accessed after the function returns. From ec27f3b700010d2ebae1cde2df0246197689d159 Mon Sep 17 00:00:00 2001 From: Yilin Chen <1479826151@qq.com> Date: Mon, 14 Sep 2026 14:09:00 +0000 Subject: [PATCH 02/13] rust: configfs: require thread-safe callback data The Rust configfs abstractions do not fully constrain callback data for cross-thread use. Add the missing `Send` and `Sync` requirements. Specifically, make the following changes: 1. Require `Data: Send` when implementing `Send` for `Subsystem`, since the subsystem stores its data by value. 2. Make `GroupOperations` a `Sync` supertrait because `make_group` and `drop_item` receive `&self` from foreign threads. Require `Child: Send` because configfs may release child groups on an arbitrary thread. 3. Require `AttributeOperations::Data: Sync` because its callbacks receive `&Data` from foreign threads. 4. Update the safety comments in FFI callbacks that call `get_group_data` to cite these bounds as justification for sharing the returned references with the callback thread. 5. Remove redundant `Child: 'static` bounds from `GroupOperationsVTable` and `new_with_child_ctor`. Fixes: 446cafc295bf ("rust: configfs: introduce rust support for configfs") Link: https://rust-for-linux.zulipchat.com/#narrow/channel/288089-General/topic/Should.20add.20Data.3A.20Send.2FSync.20bounds.20in.20configfs.3A.3ASubsystem.3F/with/623719979 Assisted-by: Gpt-5.6 Sol Signed-off-by: Yilin Chen <1479826151@qq.com> Link: https://msgid.link/tencent_5449081195EDDF5726C709CF648DD4044A08@qq.com [ Andreas: Add Send/Sync bounds for `Group`, add `Send` bounds to `GroupOperationsVTable`, `ItemOperationsVTable` and `ItemType` macro implementation. ] Signed-off-by: Andreas Hindborg --- rust/kernel/configfs.rs | 63 ++++++++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/rust/kernel/configfs.rs b/rust/kernel/configfs.rs index 7722035ebc02..ec295ab965da 100644 --- a/rust/kernel/configfs.rs +++ b/rust/kernel/configfs.rs @@ -135,8 +135,9 @@ pub struct Subsystem { // SAFETY: We do not provide any operations on `Subsystem`. unsafe impl Sync for Subsystem {} -// SAFETY: Ownership of `Subsystem` can safely be transferred to other threads. -unsafe impl Send for Subsystem {} +// SAFETY: Ownership of `Subsystem` can safely be transferred to other threads +// if its data can be transferred as well. +unsafe impl Send for Subsystem {} impl Subsystem { /// Create an initializer for a [`Subsystem`]. @@ -249,6 +250,13 @@ pub struct Group { data: Data, } +// SAFETY: We do not provide any operations on `Group`. +unsafe impl Sync for Group {} + +// SAFETY: Ownership of `Group` can safely be transferred to other threads if +// its data can be transferred as well. +unsafe impl Send for Group {} + impl Group { /// Create an initializer for a new group. /// @@ -325,7 +333,9 @@ unsafe fn get_group_data<'a, Parent>(this: *mut bindings::config_group) -> &'a P impl GroupOperationsVTable where Parent: GroupOperations, - Child: 'static, + // We transfer `Arc>` across a thread boundary in `make_group` + // and `drop_item`. + Arc>: Send, { /// # Safety /// @@ -344,8 +354,9 @@ impl GroupOperationsVTable this: *mut bindings::config_group, name: *const kernel::ffi::c_char, ) -> *mut bindings::config_group { - // SAFETY: By function safety requirements of this function, this call - // is safe. + // SAFETY: By function safety requirements, `this` points to a configfs + // group containing `Parent`. The `GroupOperations` bound guarantees + // that `Parent: Sync`, so it is safe to share it with this thread. let parent_data = unsafe { get_group_data(this) }; let group_init = match Parent::make_group( @@ -390,8 +401,9 @@ impl GroupOperationsVTable this: *mut bindings::config_group, item: *mut bindings::config_item, ) { - // SAFETY: By function safety requirements of this function, this call - // is safe. + // SAFETY: By function safety requirements, `this` points to a configfs + // group containing `Parent`. The `GroupOperations` bound guarantees + // that `Parent: Sync`, so it is safe to share it with this thread. let parent_data = unsafe { get_group_data(this) }; // SAFETY: By function safety requirements, `item` is embedded in a @@ -403,7 +415,9 @@ impl GroupOperationsVTable if Parent::HAS_DROP_ITEM { // SAFETY: We called `into_raw` to produce `r_child_group_ptr` in - // `make_group`. + // `make_group`. This function may be executing on a different + // thread than `into_raw`. As `Arc>: Send` this + // ownership transfer is safe. let arc: Arc> = unsafe { Arc::from_raw(r_child_group_ptr.cast_mut()) }; Parent::drop_item(parent_data, arc.as_arc_borrow()); @@ -434,6 +448,8 @@ const fn vtable_ptr() -> *const bindings::configfs_group_operations { impl ItemOperationsVTable, Data> where Data: 'static, + // We transfer `Arc>` across a thread boundary in `release`. + Arc>: Send, { /// # Safety /// @@ -450,8 +466,9 @@ impl ItemOperationsVTable, Data> // embedded within a `Group`. let r_group_ptr = unsafe { Group::::container_of(c_group_ptr) }; - // SAFETY: We called `into_raw` on `r_group_ptr` in - // `make_group`. + // SAFETY: We called `into_raw` on `r_group_ptr` in `make_group`. This + // function may be running on a different thread than the thread that + // called `into_raw`. As `Arc>: Send`, this is safe. let pin_self: Arc> = unsafe { Arc::from_raw(r_group_ptr.cast_mut()) }; drop(pin_self); } @@ -483,12 +500,12 @@ const fn vtable_ptr() -> *const bindings::configfs_item_operations { /// /// Implement this trait on structs that embed a [`Subsystem`] or a [`Group`]. #[vtable] -pub trait GroupOperations { +pub trait GroupOperations: Sync { /// The child data object type. /// /// This group will create subgroups (subdirectories) backed by this kind of /// object. - type Child: 'static; + type Child: 'static + Send; /// Creates a new subgroup. /// @@ -555,8 +572,10 @@ impl Attribute // `config_group`. unsafe { container_of!(item, bindings::config_group, cg_item) }; - // SAFETY: The function safety requirements for this function satisfy - // the conditions for this call. + // SAFETY: By function safety requirements, `c_group` points to a + // configfs group containing `Data`. The `AttributeOperations` bound + // guarantees that `Data: Sync`, so it is safe to share it with this + // thread. let data: &Data = unsafe { get_group_data(c_group) }; // SAFETY: By function safety requirements, `page` is writable for `PAGE_SIZE`. @@ -589,8 +608,10 @@ impl Attribute // `config_group`. unsafe { container_of!(item, bindings::config_group, cg_item) }; - // SAFETY: The function safety requirements for this function satisfy - // the conditions for this call. + // SAFETY: By function safety requirements, `c_group` points to a + // configfs group containing `Data`. The `AttributeOperations` bound + // guarantees that `Data: Sync`, so it is safe to share it with this + // thread. let data: &Data = unsafe { get_group_data(c_group) }; let ret = O::store( @@ -643,7 +664,7 @@ pub const fn new(name: &'static CStr) -> Self { pub trait AttributeOperations { /// The type of the object that contains the field that is backing the /// attribute for this operation. - type Data; + type Data: Sync; /// Renders the value of an attribute. /// @@ -749,7 +770,8 @@ pub const fn new_with_child_ctor( ) -> Self where Data: GroupOperations, - Child: 'static, + Arc>: Send, + Arc>: Send, { Self { item_type: Opaque::new(bindings::config_item_type { @@ -767,7 +789,10 @@ pub const fn new_with_child_ctor( pub const fn new( owner: &'static ThisModule, attributes: &'static AttributeList, - ) -> Self { + ) -> Self + where + Arc>: Send, + { Self { item_type: Opaque::new(bindings::config_item_type { ct_owner: owner.as_ptr(), From e84c9b64df615e658e47561b835c1364dc48f0c0 Mon Sep 17 00:00:00 2001 From: Andreas Hindborg Date: Mon, 8 Jun 2026 14:32:00 +0200 Subject: [PATCH 03/13] rust: configfs: fix data offset calculation for subsystem callbacks `get_group_data()` chooses between `Group::::container_of()` and `Subsystem::::container_of()` based on whether `this` represents the root group of a configfs subsystem. It detects this by checking `(*this).cg_subsys.is_null()`, but `link_group()` in `fs/configfs/dir.c` unconditionally sets `cg_subsys` for every `config_group` attached anywhere in a registered subsystem, including the subsystem's own `su_group`. The only `config_group` with a NULL `cg_subsys` is the configfs root, on which userspace cannot trigger callbacks. The check is therefore always false at runtime, and the `Subsystem` branch is dead. Subsystem-level callbacks reach the `Group` branch and may read `data` at the wrong offset. Thus change the `is_root` check to correctly identify whether a group is a root group by comparing `this` to `subsys.su_group`. Cc: stable@vger.kernel.org Fixes: 446cafc295bf ("rust: configfs: introduce rust support for configfs") Link: https://msgid.link/20260608-configfs-fix-offset-v1-1-7f01b8fb9e5f@kernel.org [ Andreas: Fix clippy warning on 1.85.0 by moving safety comment. ] Signed-off-by: Andreas Hindborg --- rust/kernel/configfs.rs | 43 +++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/rust/kernel/configfs.rs b/rust/kernel/configfs.rs index ec295ab965da..0f506d3a3cf5 100644 --- a/rust/kernel/configfs.rs +++ b/rust/kernel/configfs.rs @@ -305,26 +305,39 @@ unsafe fn container_of(group: *const bindings::config_group) -> *const Self { /// /// `this` must be a valid pointer. /// -/// If `this` does not represent the root group of a configfs subsystem, -/// `this` must be a pointer to a `bindings::config_group` embedded in a -/// `Group`. +/// If `this` is the `su_group` field of a `bindings::configfs_subsystem`, that +/// `configfs_subsystem` must be embedded in a `Subsystem`. /// -/// Otherwise, `this` must be a pointer to a `bindings::config_group` that -/// is embedded in a `bindings::configfs_subsystem` that is embedded in a -/// `Subsystem`. +/// Otherwise, `this` must be a pointer to a `bindings::config_group` embedded +/// in a `Group`. unsafe fn get_group_data<'a, Parent>(this: *mut bindings::config_group) -> &'a Parent { // SAFETY: `this` is a valid pointer. - let is_root = unsafe { (*this).cg_subsys.is_null() }; + let subsys = unsafe { (*this).cg_subsys }; - if !is_root { - // SAFETY: By C API contact,`this` was returned from a call to - // `make_group`. The pointer is known to be embedded within a - // `Group`. - unsafe { &(*Group::::container_of(this)).data } - } else { - // SAFETY: By C API contract, `this` is a pointer to the - // `bindings::config_group` field within a `Subsystem`. + // `link_group()` in `fs/configfs/dir.c` assigns `cg_subsys` for every + // `config_group` attached anywhere in a registered subsystem, including + // the subsystem's own `su_group` (which gets a pointer to itself). The + // only `config_group` with a NULL `cg_subsys` is the configfs root, and + // userspace cannot trigger callbacks on it. The group is therefore the + // subsystem's `su_group` iff it equals `&cg_subsys->su_group`. + let is_root = !subsys.is_null() + && core::ptr::eq( + this.cast_const(), + // SAFETY: For every `config_group` the configfs core dispatches a + // callback on, `cg_subsys` was set by `link_group()` at registration time + // and points to a valid `configfs_subsystem` that outlives the callback. + unsafe { &raw const (*subsys).su_group }, + ); + + if is_root { + // SAFETY: By the above, `this` is the `su_group` field of a + // `configfs_subsystem` that, by function safety requirements, is + // embedded in a `Subsystem`. unsafe { &(*Subsystem::container_of(this)).data } + } else { + // SAFETY: By function safety requirements, `this` is a + // `config_group` embedded in a `Group`. + unsafe { &(*Group::::container_of(this)).data } } } From a7fdcb2edd0fe797bade7e81fd9078f00a98e367 Mon Sep 17 00:00:00 2001 From: Breno Leitao Date: Wed, 26 Aug 2026 03:47:02 -0700 Subject: [PATCH 04/13] selftests/configfs: add tests for the userspace interface 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. Link: https://patch.msgid.link/20260826-config_selftest-v1-1-e364b07e91ac@debian.org Signed-off-by: Breno Leitao --- MAINTAINERS | 1 + tools/testing/selftests/Makefile | 1 + .../selftests/filesystems/configfs/.gitignore | 2 + .../selftests/filesystems/configfs/Makefile | 8 + .../selftests/filesystems/configfs/config | 5 + .../filesystems/configfs/configfs_test.c | 338 ++++++++++++++++++ 6 files changed, 355 insertions(+) create mode 100644 tools/testing/selftests/filesystems/configfs/.gitignore create mode 100644 tools/testing/selftests/filesystems/configfs/Makefile create mode 100644 tools/testing/selftests/filesystems/configfs/config create mode 100644 tools/testing/selftests/filesystems/configfs/configfs_test.c diff --git a/MAINTAINERS b/MAINTAINERS index f53047aa8d3c..8d60a6c89385 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -6605,6 +6605,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 bc4fdb6f5337..89e175372d24 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/eventfd TARGETS += filesystems/failfs diff --git a/tools/testing/selftests/filesystems/configfs/.gitignore b/tools/testing/selftests/filesystems/configfs/.gitignore new file mode 100644 index 000000000000..accfb6bb4826 --- /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 000000000000..359296356c83 --- /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 000000000000..5ea17df535b3 --- /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 000000000000..072d0dcf3f96 --- /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 From 0a51cf54232ecdfb8172ad7943e04e67e401538a Mon Sep 17 00:00:00 2001 From: Breno Leitao Date: Wed, 26 Aug 2026 03:47:03 -0700 Subject: [PATCH 05/13] samples/configfs: add a subsystem that accepts symlinks The sample registers three subsystems and none implements ->allow_link(), so the symlink(2) half of the configfs ABI has no small in-tree user to test against. Add 04-symlink-children, whose children take a link to any config_item and export the number of links they hold. Link: https://patch.msgid.link/20260826-config_selftest-v1-2-e364b07e91ac@debian.org Signed-off-by: Breno Leitao --- samples/configfs/configfs_sample.c | 121 +++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/samples/configfs/configfs_sample.c b/samples/configfs/configfs_sample.c index c1b108ec4ea0..08403e787aef 100644 --- a/samples/configfs/configfs_sample.c +++ b/samples/configfs/configfs_sample.c @@ -313,6 +313,126 @@ static struct configfs_subsystem group_children_subsys = { /* ----------------------------------------------------------------- */ +/* + * 04-symlink-children + * + * This example has children that are valid sources for symlink(2). A + * child accepts a link to any other config_item and reports how many + * links it currently holds, so ->allow_link() and ->drop_link() are + * observable from userspace. + */ + +struct symlink_child { + struct config_item item; + int nlinks; +}; + +static inline struct symlink_child *to_symlink_child(struct config_item *item) +{ + return container_of(item, struct symlink_child, item); +} + +static ssize_t symlink_child_nlinks_show(struct config_item *item, char *page) +{ + return sprintf(page, "%d\n", to_symlink_child(item)->nlinks); +} + +CONFIGFS_ATTR_RO(symlink_child_, nlinks); + +static struct configfs_attribute *symlink_child_attrs[] = { + &symlink_child_attr_nlinks, + NULL, +}; + +/* + * The VFS holds the source item's directory locked across symlink(2) and + * unlink(2), so ->nlinks needs no lock of its own. + */ +static int symlink_child_allow_link(struct config_item *src, + struct config_item *target) +{ + to_symlink_child(src)->nlinks++; + + return 0; +} + +static void symlink_child_drop_link(struct config_item *src, + struct config_item *target) +{ + to_symlink_child(src)->nlinks--; +} + +static void symlink_child_release(struct config_item *item) +{ + kfree(to_symlink_child(item)); +} + +static const struct configfs_item_operations symlink_child_item_ops = { + .release = symlink_child_release, + .allow_link = symlink_child_allow_link, + .drop_link = symlink_child_drop_link, +}; + +static const struct config_item_type symlink_child_type = { + .ct_item_ops = &symlink_child_item_ops, + .ct_attrs = symlink_child_attrs, + .ct_owner = THIS_MODULE, +}; + +static struct config_item *symlink_children_make_item( + struct config_group *group, const char *name) +{ + struct symlink_child *symlink_child; + + symlink_child = kzalloc_obj(*symlink_child, GFP_KERNEL); + if (!symlink_child) + return ERR_PTR(-ENOMEM); + + config_item_init_type_name(&symlink_child->item, name, + &symlink_child_type); + + return &symlink_child->item; +} + +static ssize_t symlink_children_description_show(struct config_item *item, + char *page) +{ + return sprintf(page, +"[04-symlink-children]\n" +"\n" +"This subsystem allows the creation of child config_items that\n" +"symlink(2) can point at other config_items from. Each child\n" +"reports the number of links it holds.\n"); +} + +CONFIGFS_ATTR_RO(symlink_children_, description); + +static struct configfs_attribute *symlink_children_attrs[] = { + &symlink_children_attr_description, + NULL, +}; + +static const struct configfs_group_operations symlink_children_group_ops = { + .make_item = symlink_children_make_item, +}; + +static const struct config_item_type symlink_children_type = { + .ct_group_ops = &symlink_children_group_ops, + .ct_attrs = symlink_children_attrs, + .ct_owner = THIS_MODULE, +}; + +static struct configfs_subsystem symlink_children_subsys = { + .su_group = { + .cg_item = { + .ci_namebuf = "04-symlink-children", + .ci_type = &symlink_children_type, + }, + }, +}; + +/* ----------------------------------------------------------------- */ + /* * We're now done with our subsystem definitions. * For convenience in this module, here's a list of them all. It @@ -324,6 +444,7 @@ static struct configfs_subsystem *example_subsys[] = { &childless_subsys.subsys, &simple_children_subsys, &group_children_subsys, + &symlink_children_subsys, NULL, }; From 77b20a890a20c943f55d4120f0bcba838b827874 Mon Sep 17 00:00:00 2001 From: Breno Leitao Date: Wed, 26 Aug 2026 03:47:04 -0700 Subject: [PATCH 06/13] selftests/configfs: cover symlink and unlink samples/configfs had nothing to link from, so symlink(2) went untested beyond the EPERM a subsystem without ->allow_link() returns. Cover creating and removing a link, the relative body configfs stores, the count ->allow_link() and ->drop_link() keep, the busy errors both ends return, and what get_target() rejects. Link: https://patch.msgid.link/20260826-config_selftest-v1-3-e364b07e91ac@debian.org Signed-off-by: Breno Leitao --- .../filesystems/configfs/configfs_test.c | 85 ++++++++++++++++++- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/tools/testing/selftests/filesystems/configfs/configfs_test.c b/tools/testing/selftests/filesystems/configfs/configfs_test.c index 072d0dcf3f96..9b15e1fd69e5 100644 --- a/tools/testing/selftests/filesystems/configfs/configfs_test.c +++ b/tools/testing/selftests/filesystems/configfs/configfs_test.c @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0 /* - * Exercise the configfs userspace interface through the three subsystems + * Exercise the configfs userspace interface through the subsystems * registered by samples/configfs. * * Copyright (c) 2026 Meta Platforms, Inc. and affiliates @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -31,16 +32,24 @@ #define CHILDLESS "01-childless" #define SIMPLE "02-simple-children" #define GROUPS "03-group-children" +#define SYMLINKS "04-symlink-children" #define ITEM_A SIMPLE "/kselftest-a" #define ITEM_B SIMPLE "/kselftest-b" #define GROUP GROUPS "/kselftest-group" #define GROUP_ITEM GROUP "/kselftest-a" +#define LINK_SRC SYMLINKS "/kselftest-src" +#define LINK LINK_SRC "/kselftest-link" + +static const char * const test_links[] = { + LINK, +}; /* Deepest first, so one pass empties the tree. */ static const char * const test_dirs[] = { GROUP_ITEM, GROUP, + LINK_SRC, ITEM_A, ITEM_B, }; @@ -49,6 +58,10 @@ static void drop_test_dirs(void) { size_t i; + /* Links first: they hold both their source and their target. */ + for (i = 0; i < ARRAY_SIZE(test_links); i++) + unlink(test_links[i]); + for (i = 0; i < ARRAY_SIZE(test_dirs); i++) rmdir(test_dirs[i]); } @@ -131,7 +144,7 @@ FIXTURE_TEARDOWN(configfs) TEST_F(configfs, mount_and_subsystems) { - const char * const subsys[] = { CHILDLESS, SIMPLE, GROUPS }; + const char * const subsys[] = { CHILDLESS, SIMPLE, GROUPS, SYMLINKS }; struct statfs sfs; struct stat st; size_t i; @@ -324,6 +337,74 @@ TEST_F(configfs, symlink_without_allow_link) EXPECT_EQ(errno, EPERM); } +TEST_F(configfs, symlink_and_unlink) +{ + char buf[PATH_MAX]; + struct stat st; + ssize_t n; + + ASSERT_EQ(mkdir(ITEM_A, 0755), 0); + ASSERT_EQ(mkdir(LINK_SRC, 0755), 0); + + ASSERT_EQ(symlink(ITEM_A, LINK), 0); + + /* configfs stores its own body, a path relative to the link. */ + n = readlink(LINK, buf, sizeof(buf) - 1); + ASSERT_GT(n, 0); + buf[n] = '\0'; + EXPECT_STREQ(buf, "../../" ITEM_A); + EXPECT_EQ(stat(LINK "/storeme", &st), 0); + + /* ->allow_link() ran on the source, not on the target. */ + ASSERT_GT(read_attr(LINK_SRC "/nlinks", buf, sizeof(buf)), 0); + EXPECT_STREQ(buf, "1\n"); + + ASSERT_EQ(unlink(LINK), 0); + ASSERT_GT(read_attr(LINK_SRC "/nlinks", buf, sizeof(buf)), 0); + EXPECT_STREQ(buf, "0\n"); +} + +TEST_F(configfs, symlink_pins_both_ends) +{ + ASSERT_EQ(mkdir(ITEM_A, 0755), 0); + ASSERT_EQ(mkdir(LINK_SRC, 0755), 0); + ASSERT_EQ(symlink(ITEM_A, LINK), 0); + + /* A linked item cannot go away under the link. */ + ASSERT_EQ(rmdir(ITEM_A), -1); + EXPECT_EQ(errno, EBUSY); + + /* The link counts as a child of its source. */ + ASSERT_EQ(rmdir(LINK_SRC), -1); + EXPECT_EQ(errno, ENOTEMPTY); + + ASSERT_EQ(unlink(LINK), 0); + EXPECT_EQ(rmdir(ITEM_A), 0); +} + +TEST_F(configfs, symlink_target_outside_configfs) +{ + ASSERT_EQ(mkdir(LINK_SRC, 0755), 0); + ASSERT_EQ(symlink("/", LINK), -1); + EXPECT_EQ(errno, EPERM); +} + +TEST_F(configfs, symlink_target_missing) +{ + ASSERT_EQ(mkdir(LINK_SRC, 0755), 0); + ASSERT_EQ(symlink(SIMPLE "/kselftest-gone", LINK), -1); + EXPECT_EQ(errno, ENOENT); +} + +TEST_F(configfs, symlink_target_is_an_attribute) +{ + ASSERT_EQ(mkdir(LINK_SRC, 0755), 0); + + /* The target is resolved with LOOKUP_DIRECTORY. */ + ASSERT_EQ(symlink(CHILDLESS "/storeme", LINK), -1); + EXPECT_EQ(errno, ENOTDIR); +} + TEST_F(configfs, module_pinned_by_item) { ASSERT_EQ(mkdir(ITEM_A, 0755), 0); From 6b1b324e96dce23de8e204e277258ddc46d8d2ca Mon Sep 17 00:00:00 2001 From: Breno Leitao Date: Wed, 26 Aug 2026 03:47:05 -0700 Subject: [PATCH 07/13] selftests/configfs: race symlink against rmdir of the target 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] Link: https://patch.msgid.link/20260826-config_selftest-v1-4-e364b07e91ac@debian.org Signed-off-by: Breno Leitao --- .../selftests/filesystems/configfs/Makefile | 2 +- .../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 359296356c83..40a91ed788ed 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 9b15e1fd69e5..c6a1049e5852 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); From e24fdc27dc37a816cfc7b08ff9e1664b6ec8ca7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9Fschuh?= Date: Thu, 16 Jul 2026 19:09:26 +0200 Subject: [PATCH 08/13] rust: configfs: remove mutability from some field initializers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These fields do not require mutable pointers. Use regular immutable ones. Signed-off-by: Thomas Weißschuh Acked-by: Andreas Hindborg Link: https://patch.msgid.link/20260716-configfs-const-base-v1-1-c545a4053cb5@weissschuh.net Signed-off-by: Breno Leitao --- rust/kernel/configfs.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rust/kernel/configfs.rs b/rust/kernel/configfs.rs index cd082b83e9e7..5c66b7e4b109 100644 --- a/rust/kernel/configfs.rs +++ b/rust/kernel/configfs.rs @@ -754,8 +754,8 @@ pub const fn new_with_child_ctor( Self { item_type: Opaque::new(bindings::config_item_type { ct_owner: owner.as_ptr(), - ct_group_ops: GroupOperationsVTable::::vtable_ptr().cast_mut(), - ct_item_ops: ItemOperationsVTable::<$tpe, Data>::vtable_ptr().cast_mut(), + ct_group_ops: GroupOperationsVTable::::vtable_ptr(), + ct_item_ops: ItemOperationsVTable::<$tpe, Data>::vtable_ptr(), ct_attrs: core::ptr::from_ref(attributes).cast_mut().cast(), ct_bin_attrs: core::ptr::null_mut(), }), @@ -771,8 +771,8 @@ pub const fn new( Self { item_type: Opaque::new(bindings::config_item_type { ct_owner: owner.as_ptr(), - ct_group_ops: core::ptr::null_mut(), - ct_item_ops: ItemOperationsVTable::<$tpe, Data>::vtable_ptr().cast_mut(), + ct_group_ops: core::ptr::null(), + ct_item_ops: ItemOperationsVTable::<$tpe, Data>::vtable_ptr(), ct_attrs: core::ptr::from_ref(attributes).cast_mut().cast(), ct_bin_attrs: core::ptr::null_mut(), }), From 55389a4354a643492bd0f6b2a06c6e510c4e7905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9Fschuh?= Date: Thu, 16 Jul 2026 19:09:27 +0200 Subject: [PATCH 09/13] configfs: Constify is_visible/is_visible_bin in configfs_group_operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These callbacks are never meant to modify their configfs_attribute structure. Enforce this in the type system. As there are only two implementers of these callbacks, adapt them right away, avoiding a phased transition. Signed-off-by: Thomas Weißschuh Reviewed-by: Breno Leitao Acked-by: Andreas Hindborg Link: https://patch.msgid.link/20260716-configfs-const-base-v1-2-c545a4053cb5@weissschuh.net Signed-off-by: Breno Leitao --- drivers/gpu/drm/xe/xe_configfs.c | 4 ++-- drivers/virt/coco/guest/report.c | 4 ++-- include/linux/configfs.h | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/drivers/gpu/drm/xe/xe_configfs.c b/drivers/gpu/drm/xe/xe_configfs.c index 052cce962161..842c180b2e85 100644 --- a/drivers/gpu/drm/xe/xe_configfs.c +++ b/drivers/gpu/drm/xe/xe_configfs.c @@ -886,7 +886,7 @@ static struct configfs_item_operations xe_config_device_ops = { }; static bool xe_config_device_is_visible(struct config_item *item, - struct configfs_attribute *attr, int n) + const struct configfs_attribute *attr, int n) { struct xe_config_group_device *dev = to_xe_config_group_device(item); @@ -981,7 +981,7 @@ static struct configfs_attribute *xe_config_sriov_attrs[] = { }; static bool xe_config_sriov_is_visible(struct config_item *item, - struct configfs_attribute *attr, int n) + const struct configfs_attribute *attr, int n) { struct xe_config_group_device *dev = to_xe_config_group_device(item->ci_parent); diff --git a/drivers/virt/coco/guest/report.c b/drivers/virt/coco/guest/report.c index b254a1416286..96e89ddf4989 100644 --- a/drivers/virt/coco/guest/report.c +++ b/drivers/virt/coco/guest/report.c @@ -381,7 +381,7 @@ static struct configfs_item_operations tsm_report_item_ops = { }; static bool tsm_report_is_visible(struct config_item *item, - struct configfs_attribute *attr, int n) + const struct configfs_attribute *attr, int n) { guard(rwsem_read)(&tsm_rwsem); if (!provider.ops) @@ -394,7 +394,7 @@ static bool tsm_report_is_visible(struct config_item *item, } static bool tsm_report_is_bin_visible(struct config_item *item, - struct configfs_bin_attribute *attr, int n) + const struct configfs_bin_attribute *attr, int n) { guard(rwsem_read)(&tsm_rwsem); if (!provider.ops) diff --git a/include/linux/configfs.h b/include/linux/configfs.h index ef65c75beeaa..5d3fc8822a1d 100644 --- a/include/linux/configfs.h +++ b/include/linux/configfs.h @@ -220,8 +220,8 @@ struct configfs_group_operations { struct config_group *(*make_group)(struct config_group *group, const char *name); void (*disconnect_notify)(struct config_group *group, struct config_item *item); void (*drop_item)(struct config_group *group, struct config_item *item); - bool (*is_visible)(struct config_item *item, struct configfs_attribute *attr, int n); - bool (*is_bin_visible)(struct config_item *item, struct configfs_bin_attribute *attr, + bool (*is_visible)(struct config_item *item, const struct configfs_attribute *attr, int n); + bool (*is_bin_visible)(struct config_item *item, const struct configfs_bin_attribute *attr, int n); }; From 459d4b9a893f53d5635f24f44464f1988aaaf07e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9Fschuh?= Date: Thu, 16 Jul 2026 19:09:28 +0200 Subject: [PATCH 10/13] configfs: Treat attribute structures as const internally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The configfs core never modifies the attribute structures defined in driver core. Reflect this in the types used internally in the configfs core. Signed-off-by: Thomas Weißschuh Reviewed-by: Breno Leitao Acked-by: Andreas Hindborg Link: https://patch.msgid.link/20260716-configfs-const-base-v1-3-c545a4053cb5@weissschuh.net Signed-off-by: Breno Leitao --- fs/configfs/configfs_internal.h | 10 +++++----- fs/configfs/dir.c | 6 +++--- fs/configfs/file.c | 6 +++--- fs/configfs/inode.c | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/fs/configfs/configfs_internal.h b/fs/configfs/configfs_internal.h index acdeea8e2d69..4bc19cd8d666 100644 --- a/fs/configfs/configfs_internal.h +++ b/fs/configfs/configfs_internal.h @@ -104,17 +104,17 @@ static inline struct config_item * to_item(struct dentry * dentry) return ((struct config_item *) sd->s_element); } -static inline struct configfs_attribute * to_attr(struct dentry * dentry) +static inline const struct configfs_attribute * to_attr(struct dentry * dentry) { struct configfs_dirent * sd = dentry->d_fsdata; - return ((struct configfs_attribute *) sd->s_element); + return ((const struct configfs_attribute *) sd->s_element); } -static inline struct configfs_bin_attribute *to_bin_attr(struct dentry *dentry) +static inline const struct configfs_bin_attribute *to_bin_attr(struct dentry *dentry) { - struct configfs_attribute *attr = to_attr(dentry); + const struct configfs_attribute *attr = to_attr(dentry); - return container_of(attr, struct configfs_bin_attribute, cb_attr); + return container_of_const(attr, struct configfs_bin_attribute, cb_attr); } static inline struct config_item *configfs_get_config_item(struct dentry *dentry) diff --git a/fs/configfs/dir.c b/fs/configfs/dir.c index eda80c2a2d38..655e361b27a8 100644 --- a/fs/configfs/dir.c +++ b/fs/configfs/dir.c @@ -470,7 +470,7 @@ static struct dentry * configfs_lookup(struct inode *dir, */ if ((sd->s_type & CONFIGFS_NOT_PINNED) && !strcmp(configfs_get_name(sd), dentry->d_name.name)) { - struct configfs_attribute *attr = sd->s_element; + const struct configfs_attribute *attr = sd->s_element; umode_t mode = (attr->ca_mode & S_IALLUGO) | S_IFREG; dentry->d_fsdata = configfs_get(sd); @@ -631,8 +631,8 @@ static int populate_attrs(struct config_item *item) { const struct config_item_type *t = item->ci_type; const struct configfs_group_operations *ops; - struct configfs_attribute *attr; - struct configfs_bin_attribute *bin_attr; + const struct configfs_attribute *attr; + const struct configfs_bin_attribute *bin_attr; int error = 0; int i; diff --git a/fs/configfs/file.c b/fs/configfs/file.c index a48cece775a3..6460b000c593 100644 --- a/fs/configfs/file.c +++ b/fs/configfs/file.c @@ -41,8 +41,8 @@ struct configfs_buffer { struct config_item *item; struct module *owner; union { - struct configfs_attribute *attr; - struct configfs_bin_attribute *bin_attr; + const struct configfs_attribute *attr; + const struct configfs_bin_attribute *bin_attr; }; }; @@ -291,7 +291,7 @@ static int __configfs_open_file(struct inode *inode, struct file *file, int type { struct dentry *dentry = file->f_path.dentry; struct configfs_fragment *frag = to_frag(file); - struct configfs_attribute *attr; + const struct configfs_attribute *attr; struct configfs_buffer *buffer; int error; diff --git a/fs/configfs/inode.c b/fs/configfs/inode.c index 68290fe0e374..69f1f24e890f 100644 --- a/fs/configfs/inode.c +++ b/fs/configfs/inode.c @@ -178,7 +178,7 @@ struct inode *configfs_create(struct dentry *dentry, umode_t mode) */ const unsigned char * configfs_get_name(struct configfs_dirent *sd) { - struct configfs_attribute *attr; + const struct configfs_attribute *attr; BUG_ON(!sd || !sd->s_element); From ee4f267ada12bd5eb8b675018bdd60e60da2c089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9Fschuh?= Date: Thu, 16 Jul 2026 19:09:29 +0200 Subject: [PATCH 11/13] configfs: Constify configfs_bin_attribute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The configfs_bin_attribute structures defined by driver are never modified. Make them const. As there are only two users of these attributes, adapt them in the same commit to avoid a phased transition. Signed-off-by: Thomas Weißschuh Acked-by: Andreas Hindborg Link: https://patch.msgid.link/20260716-configfs-const-base-v1-4-c545a4053cb5@weissschuh.net Signed-off-by: Breno Leitao --- drivers/acpi/acpi_configfs.c | 2 +- drivers/virt/coco/guest/report.c | 2 +- include/linux/configfs.h | 64 ++++++++++++++++---------------- rust/kernel/configfs.rs | 4 +- 4 files changed, 36 insertions(+), 36 deletions(-) diff --git a/drivers/acpi/acpi_configfs.c b/drivers/acpi/acpi_configfs.c index 12ffec795803..6071699c7165 100644 --- a/drivers/acpi/acpi_configfs.c +++ b/drivers/acpi/acpi_configfs.c @@ -91,7 +91,7 @@ static ssize_t acpi_table_aml_read(struct config_item *cfg, CONFIGFS_BIN_ATTR(acpi_table_, aml, NULL, MAX_ACPI_TABLE_SIZE); -static struct configfs_bin_attribute *acpi_table_bin_attrs[] = { +static const struct configfs_bin_attribute *const acpi_table_bin_attrs[] = { &acpi_table_attr_aml, NULL, }; diff --git a/drivers/virt/coco/guest/report.c b/drivers/virt/coco/guest/report.c index 96e89ddf4989..ad400fbe53f0 100644 --- a/drivers/virt/coco/guest/report.c +++ b/drivers/virt/coco/guest/report.c @@ -356,7 +356,7 @@ static struct configfs_attribute *tsm_report_attrs[] = { NULL, }; -static struct configfs_bin_attribute *tsm_report_bin_attrs[] = { +static const struct configfs_bin_attribute *const tsm_report_bin_attrs[] = { [TSM_REPORT_INBLOB] = &tsm_report_attr_inblob, [TSM_REPORT_OUTBLOB] = &tsm_report_attr_outblob, [TSM_REPORT_AUXBLOB] = &tsm_report_attr_auxblob, diff --git a/include/linux/configfs.h b/include/linux/configfs.h index 5d3fc8822a1d..eff2fb22ab70 100644 --- a/include/linux/configfs.h +++ b/include/linux/configfs.h @@ -67,7 +67,7 @@ struct config_item_type { const struct configfs_item_operations *ct_item_ops; const struct configfs_group_operations *ct_group_ops; struct configfs_attribute **ct_attrs; - struct configfs_bin_attribute **ct_bin_attrs; + const struct configfs_bin_attribute *const *ct_bin_attrs; }; /** @@ -160,41 +160,41 @@ struct configfs_bin_attribute { ssize_t (*write)(struct config_item *, const void *, size_t); }; -#define CONFIGFS_BIN_ATTR(_pfx, _name, _priv, _maxsz) \ -static struct configfs_bin_attribute _pfx##attr_##_name = { \ - .cb_attr = { \ - .ca_name = __stringify(_name), \ - .ca_mode = S_IRUGO | S_IWUSR, \ - .ca_owner = THIS_MODULE, \ - }, \ - .cb_private = _priv, \ - .cb_max_size = _maxsz, \ - .read = _pfx##_name##_read, \ - .write = _pfx##_name##_write, \ +#define CONFIGFS_BIN_ATTR(_pfx, _name, _priv, _maxsz) \ +static const struct configfs_bin_attribute _pfx##attr_##_name = { \ + .cb_attr = { \ + .ca_name = __stringify(_name), \ + .ca_mode = S_IRUGO | S_IWUSR, \ + .ca_owner = THIS_MODULE, \ + }, \ + .cb_private = _priv, \ + .cb_max_size = _maxsz, \ + .read = _pfx##_name##_read, \ + .write = _pfx##_name##_write, \ } -#define CONFIGFS_BIN_ATTR_RO(_pfx, _name, _priv, _maxsz) \ -static struct configfs_bin_attribute _pfx##attr_##_name = { \ - .cb_attr = { \ - .ca_name = __stringify(_name), \ - .ca_mode = S_IRUGO, \ - .ca_owner = THIS_MODULE, \ - }, \ - .cb_private = _priv, \ - .cb_max_size = _maxsz, \ - .read = _pfx##_name##_read, \ +#define CONFIGFS_BIN_ATTR_RO(_pfx, _name, _priv, _maxsz) \ +static const struct configfs_bin_attribute _pfx##attr_##_name = { \ + .cb_attr = { \ + .ca_name = __stringify(_name), \ + .ca_mode = S_IRUGO, \ + .ca_owner = THIS_MODULE, \ + }, \ + .cb_private = _priv, \ + .cb_max_size = _maxsz, \ + .read = _pfx##_name##_read, \ } -#define CONFIGFS_BIN_ATTR_WO(_pfx, _name, _priv, _maxsz) \ -static struct configfs_bin_attribute _pfx##attr_##_name = { \ - .cb_attr = { \ - .ca_name = __stringify(_name), \ - .ca_mode = S_IWUSR, \ - .ca_owner = THIS_MODULE, \ - }, \ - .cb_private = _priv, \ - .cb_max_size = _maxsz, \ - .write = _pfx##_name##_write, \ +#define CONFIGFS_BIN_ATTR_WO(_pfx, _name, _priv, _maxsz) \ +static const struct configfs_bin_attribute _pfx##attr_##_name = { \ + .cb_attr = { \ + .ca_name = __stringify(_name), \ + .ca_mode = S_IWUSR, \ + .ca_owner = THIS_MODULE, \ + }, \ + .cb_private = _priv, \ + .cb_max_size = _maxsz, \ + .write = _pfx##_name##_write, \ } /* diff --git a/rust/kernel/configfs.rs b/rust/kernel/configfs.rs index 5c66b7e4b109..5052c1bbaf94 100644 --- a/rust/kernel/configfs.rs +++ b/rust/kernel/configfs.rs @@ -757,7 +757,7 @@ pub const fn new_with_child_ctor( ct_group_ops: GroupOperationsVTable::::vtable_ptr(), ct_item_ops: ItemOperationsVTable::<$tpe, Data>::vtable_ptr(), ct_attrs: core::ptr::from_ref(attributes).cast_mut().cast(), - ct_bin_attrs: core::ptr::null_mut(), + ct_bin_attrs: core::ptr::null(), }), _p: PhantomData, } @@ -774,7 +774,7 @@ pub const fn new( ct_group_ops: core::ptr::null(), ct_item_ops: ItemOperationsVTable::<$tpe, Data>::vtable_ptr(), ct_attrs: core::ptr::from_ref(attributes).cast_mut().cast(), - ct_bin_attrs: core::ptr::null_mut(), + ct_bin_attrs: core::ptr::null(), }), _p: PhantomData, } From f7528cd38b87fb59b6e5f96abfd119f0318a52e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9Fschuh?= Date: Thu, 16 Jul 2026 19:09:30 +0200 Subject: [PATCH 12/13] configfs: Allow the registration of const struct configfs_attribute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The attribute structure defined in driver code never need to be modified. Allow them to be marked as const. As there are many drivers which use these attributes, prepare for a phased transition by using a union of const and non-const attributes. After all drivers have been migrated to the const variant, the non-const one can be replaced by the const one and the transition machinery will be removed again. Signed-off-by: Thomas Weißschuh Acked-by: Andreas Hindborg Link: https://patch.msgid.link/20260716-configfs-const-base-v1-5-c545a4053cb5@weissschuh.net Signed-off-by: Breno Leitao --- fs/configfs/dir.c | 4 ++-- include/linux/configfs.h | 5 ++++- rust/kernel/configfs.rs | 8 ++++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/fs/configfs/dir.c b/fs/configfs/dir.c index 655e361b27a8..cb45e151d852 100644 --- a/fs/configfs/dir.c +++ b/fs/configfs/dir.c @@ -641,8 +641,8 @@ static int populate_attrs(struct config_item *item) ops = t->ct_group_ops; - if (t->ct_attrs) { - for (i = 0; (attr = t->ct_attrs[i]) != NULL; i++) { + if (t->ct_attrs_const) { + for (i = 0; (attr = t->ct_attrs_const[i]) != NULL; i++) { if (ops && ops->is_visible && !ops->is_visible(item, attr, i)) continue; diff --git a/include/linux/configfs.h b/include/linux/configfs.h index eff2fb22ab70..5bead9173ec1 100644 --- a/include/linux/configfs.h +++ b/include/linux/configfs.h @@ -66,7 +66,10 @@ struct config_item_type { struct module *ct_owner; const struct configfs_item_operations *ct_item_ops; const struct configfs_group_operations *ct_group_ops; - struct configfs_attribute **ct_attrs; + union { + struct configfs_attribute **ct_attrs; + const struct configfs_attribute *const *ct_attrs_const; + }; const struct configfs_bin_attribute *const *ct_bin_attrs; }; diff --git a/rust/kernel/configfs.rs b/rust/kernel/configfs.rs index 5052c1bbaf94..0be10cf61927 100644 --- a/rust/kernel/configfs.rs +++ b/rust/kernel/configfs.rs @@ -756,7 +756,9 @@ pub const fn new_with_child_ctor( ct_owner: owner.as_ptr(), ct_group_ops: GroupOperationsVTable::::vtable_ptr(), ct_item_ops: ItemOperationsVTable::<$tpe, Data>::vtable_ptr(), - ct_attrs: core::ptr::from_ref(attributes).cast_mut().cast(), + __bindgen_anon_1: bindings::config_item_type__bindgen_ty_1 { + ct_attrs_const: core::ptr::from_ref(attributes).cast(), + }, ct_bin_attrs: core::ptr::null(), }), _p: PhantomData, @@ -773,7 +775,9 @@ pub const fn new( ct_owner: owner.as_ptr(), ct_group_ops: core::ptr::null(), ct_item_ops: ItemOperationsVTable::<$tpe, Data>::vtable_ptr(), - ct_attrs: core::ptr::from_ref(attributes).cast_mut().cast(), + __bindgen_anon_1: bindings::config_item_type__bindgen_ty_1 { + ct_attrs_const: core::ptr::from_ref(attributes).cast(), + }, ct_bin_attrs: core::ptr::null(), }), _p: PhantomData, From 154f74a964af59d677195618834313fb9584cf89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9Fschuh?= Date: Thu, 16 Jul 2026 19:09:31 +0200 Subject: [PATCH 13/13] samples: configfs: constify the configfs_attribute structures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To show that the transition machinery works, constify the sample code. Signed-off-by: Thomas Weißschuh Reviewed-by: Breno Leitao Acked-by: Andreas Hindborg Link: https://patch.msgid.link/20260716-configfs-const-base-v1-6-c545a4053cb5@weissschuh.net Signed-off-by: Breno Leitao --- samples/configfs/configfs_sample.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/samples/configfs/configfs_sample.c b/samples/configfs/configfs_sample.c index 08403e787aef..6e3a8e0061ab 100644 --- a/samples/configfs/configfs_sample.c +++ b/samples/configfs/configfs_sample.c @@ -84,7 +84,7 @@ CONFIGFS_ATTR_RO(childless_, showme); CONFIGFS_ATTR(childless_, storeme); CONFIGFS_ATTR_RO(childless_, description); -static struct configfs_attribute *childless_attrs[] = { +static const struct configfs_attribute *const childless_attrs[] = { &childless_attr_showme, &childless_attr_storeme, &childless_attr_description, @@ -92,7 +92,7 @@ static struct configfs_attribute *childless_attrs[] = { }; static const struct config_item_type childless_type = { - .ct_attrs = childless_attrs, + .ct_attrs_const = childless_attrs, .ct_owner = THIS_MODULE, }; @@ -148,7 +148,7 @@ static ssize_t simple_child_storeme_store(struct config_item *item, CONFIGFS_ATTR(simple_child_, storeme); -static struct configfs_attribute *simple_child_attrs[] = { +static const struct configfs_attribute *const simple_child_attrs[] = { &simple_child_attr_storeme, NULL, }; @@ -164,7 +164,7 @@ static const struct configfs_item_operations simple_child_item_ops = { static const struct config_item_type simple_child_type = { .ct_item_ops = &simple_child_item_ops, - .ct_attrs = simple_child_attrs, + .ct_attrs_const = simple_child_attrs, .ct_owner = THIS_MODULE, }; @@ -205,7 +205,7 @@ static ssize_t simple_children_description_show(struct config_item *item, CONFIGFS_ATTR_RO(simple_children_, description); -static struct configfs_attribute *simple_children_attrs[] = { +static const struct configfs_attribute *const simple_children_attrs[] = { &simple_children_attr_description, NULL, }; @@ -230,7 +230,7 @@ static const struct configfs_group_operations simple_children_group_ops = { static const struct config_item_type simple_children_type = { .ct_item_ops = &simple_children_item_ops, .ct_group_ops = &simple_children_group_ops, - .ct_attrs = simple_children_attrs, + .ct_attrs_const = simple_children_attrs, .ct_owner = THIS_MODULE, }; @@ -283,7 +283,7 @@ static ssize_t group_children_description_show(struct config_item *item, CONFIGFS_ATTR_RO(group_children_, description); -static struct configfs_attribute *group_children_attrs[] = { +static const struct configfs_attribute *const group_children_attrs[] = { &group_children_attr_description, NULL, }; @@ -298,7 +298,7 @@ static const struct configfs_group_operations group_children_group_ops = { static const struct config_item_type group_children_type = { .ct_group_ops = &group_children_group_ops, - .ct_attrs = group_children_attrs, + .ct_attrs_const = group_children_attrs, .ct_owner = THIS_MODULE, };