[PATCH 04/21] selftests/filesystems: check that an immutable inode takes no write hint
From: Christian Brauner
Date: Fri Oct 02 2026 - 09:56:38 EST
Check that an immutable inode takes no write hint.
Signed-off-by: Christian Brauner (Amutable) <brauner@xxxxxxxxxx>
---
tools/testing/selftests/filesystems/.gitignore | 1 +
tools/testing/selftests/filesystems/Makefile | 2 +-
tools/testing/selftests/filesystems/rw_hint_test.c | 129 +++++++++++++++++++++
3 files changed, 131 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/filesystems/.gitignore b/tools/testing/selftests/filesystems/.gitignore
index 9eb185fb2f9d..62f7b1c46649 100644
--- a/tools/testing/selftests/filesystems/.gitignore
+++ b/tools/testing/selftests/filesystems/.gitignore
@@ -7,3 +7,4 @@ anon_inode_test
kernfs_test
idmapped_tmpfile
ustat_test
+rw_hint_test
diff --git a/tools/testing/selftests/filesystems/Makefile b/tools/testing/selftests/filesystems/Makefile
index 03be337c1f35..93e2cc9123b0 100644
--- a/tools/testing/selftests/filesystems/Makefile
+++ b/tools/testing/selftests/filesystems/Makefile
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: GPL-2.0
CFLAGS += $(KHDR_INCLUDES)
-TEST_GEN_PROGS := devpts_pts file_stressor anon_inode_test kernfs_test fclog ustat_test
+TEST_GEN_PROGS := devpts_pts file_stressor anon_inode_test kernfs_test fclog ustat_test rw_hint_test
TEST_GEN_PROGS += idmapped_tmpfile
TEST_GEN_PROGS_EXTENDED := dnotify_test
diff --git a/tools/testing/selftests/filesystems/rw_hint_test.c b/tools/testing/selftests/filesystems/rw_hint_test.c
new file mode 100644
index 000000000000..d1930f82f63b
--- /dev/null
+++ b/tools/testing/selftests/filesystems/rw_hint_test.c
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * F_SET_RW_HINT is refused on an immutable inode. Nothing is ever written
+ * to it and it may be shared with everybody, like a namespace file or the
+ * root of an empty mount namespace.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include "../kselftest_harness.h"
+
+/* <linux/fs.h> and <linux/fcntl.h> don't mix with the libc headers */
+#ifndef FS_IOC_GETFLAGS
+#define FS_IOC_GETFLAGS _IOR('f', 1, long)
+#define FS_IOC_SETFLAGS _IOW('f', 2, long)
+#endif
+#ifndef FS_IMMUTABLE_FL
+#define FS_IMMUTABLE_FL 0x00000010
+#endif
+#ifndef F_LINUX_SPECIFIC_BASE
+#define F_LINUX_SPECIFIC_BASE 1024
+#endif
+#ifndef F_GET_RW_HINT
+#define F_GET_RW_HINT (F_LINUX_SPECIFIC_BASE + 11)
+#define F_SET_RW_HINT (F_LINUX_SPECIFIC_BASE + 12)
+#endif
+#ifndef RWH_WRITE_LIFE_SHORT
+#define RWH_WRITE_LIFE_SHORT 2
+#endif
+#ifndef UNSHARE_EMPTY_MNTNS
+#define UNSHARE_EMPTY_MNTNS 0x00100000
+#endif
+
+static int set_hint(int fd, uint64_t hint)
+{
+ return fcntl(fd, F_SET_RW_HINT, &hint);
+}
+
+static long get_hint(int fd)
+{
+ uint64_t hint;
+
+ if (fcntl(fd, F_GET_RW_HINT, &hint))
+ return -1;
+ return hint;
+}
+
+TEST(immutable_file)
+{
+ char path[] = "/tmp/rw_hint.XXXXXX";
+ int fd, flags;
+
+ if (geteuid())
+ SKIP(return, "test requires root");
+
+ fd = mkstemp(path);
+ ASSERT_GE(fd, 0);
+ unlink(path);
+ ASSERT_EQ(set_hint(fd, RWH_WRITE_LIFE_SHORT), 0);
+ EXPECT_EQ(get_hint(fd), RWH_WRITE_LIFE_SHORT);
+
+ if (ioctl(fd, FS_IOC_GETFLAGS, &flags)) {
+ close(fd);
+ SKIP(return, "no file attributes on this filesystem");
+ }
+ flags |= FS_IMMUTABLE_FL;
+ ASSERT_EQ(ioctl(fd, FS_IOC_SETFLAGS, &flags), 0);
+ EXPECT_EQ(set_hint(fd, RWH_WRITE_LIFE_SHORT), -1);
+ EXPECT_EQ(errno, EPERM);
+ flags &= ~FS_IMMUTABLE_FL;
+ ASSERT_EQ(ioctl(fd, FS_IOC_SETFLAGS, &flags), 0);
+ EXPECT_EQ(set_hint(fd, RWH_WRITE_LIFE_SHORT), 0);
+ close(fd);
+}
+
+TEST(namespace_file)
+{
+ int fd;
+
+ if (geteuid())
+ SKIP(return, "test requires root");
+
+ fd = open("/proc/self/ns/mnt", O_RDONLY | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+ EXPECT_EQ(set_hint(fd, RWH_WRITE_LIFE_SHORT), -1);
+ EXPECT_EQ(errno, EPERM);
+ close(fd);
+}
+
+TEST(empty_mntns_root)
+{
+ int status;
+ pid_t pid;
+
+ if (geteuid())
+ SKIP(return, "test requires root");
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0) {
+ int fd;
+
+ if (unshare(UNSHARE_EMPTY_MNTNS))
+ _exit(errno == EINVAL ? 100 : 1);
+ fd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if (fd < 0)
+ _exit(2);
+ if (set_hint(fd, RWH_WRITE_LIFE_SHORT) == 0)
+ _exit(3);
+ _exit(errno == EPERM ? 0 : 4);
+ }
+ ASSERT_EQ(waitpid(pid, &status, 0), pid);
+ ASSERT_TRUE(WIFEXITED(status));
+ if (WEXITSTATUS(status) == 100)
+ SKIP(return, "UNSHARE_EMPTY_MNTNS not supported");
+ EXPECT_EQ(WEXITSTATUS(status), 0);
+}
+
+TEST_HARNESS_MAIN
--
2.53.0