[PATCH v4 14/19] selftests/liveupdate: Test which tmpfs files can be preserved

From: David Matlack

Date: Tue Nov 10 2026 - 19:29:12 EST


Add tests to liveupdate.c for the checks done when preserving a tmpfs
file: A file can be preserved through a read-only fd, but not a
write-only fd. Preserving it requires permission to write to it, except
for the owner of an immutable file or of a file on a read-only
superblock. Append-only files can't be preserved, and files can be
preserved through a read-only mount. Also check that the size of a
preserved file is frozen until the file is unpreserved.

These checks don't need a kexec, so they run as part of the regular
liveupdate selftest.

Enable CONFIG_TMPFS_XATTR for FS_IOC_SETFLAGS. The test cases that use
it are skipped without it.

Signed-off-by: David Matlack <dmatlack@xxxxxxxxxx>
---
tools/testing/selftests/liveupdate/config | 1 +
.../testing/selftests/liveupdate/liveupdate.c | 303 ++++++++++++++++++
2 files changed, 304 insertions(+)

diff --git a/tools/testing/selftests/liveupdate/config b/tools/testing/selftests/liveupdate/config
index 016d009dba13..37230f4c488f 100644
--- a/tools/testing/selftests/liveupdate/config
+++ b/tools/testing/selftests/liveupdate/config
@@ -9,4 +9,5 @@ CONFIG_LIVEUPDATE=y
CONFIG_LIVEUPDATE_TEST=y
CONFIG_MEMFD_CREATE=y
CONFIG_TMPFS=y
+CONFIG_TMPFS_XATTR=y
CONFIG_SHMEM=y
diff --git a/tools/testing/selftests/liveupdate/liveupdate.c b/tools/testing/selftests/liveupdate/liveupdate.c
index 4c4272167635..3d943b5672e6 100644
--- a/tools/testing/selftests/liveupdate/liveupdate.c
+++ b/tools/testing/selftests/liveupdate/liveupdate.c
@@ -16,15 +16,24 @@
* - Resource preservation: successfully preserving individual and multiple memfds,
* verifying contents remain accessible.
* - Complex multi-session scenarios involving mixed empty and populated files.
+ * - Which tmpfs files can be preserved, and the frozen size of preserved files.
*/

#include <errno.h>
#include <fcntl.h>
+#include <grp.h>
#include <stddef.h>
+#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
#include <unistd.h>

+#include <linux/falloc.h>
+#include <linux/fs.h>
+
#include <libliveupdate.h>
#include <linux/liveupdate.h>

@@ -574,4 +583,298 @@ TEST_F(liveupdate_device, preserve_memfd_with_arg)
ASSERT_EQ(close(mem_fd2), 0);
}

+#define NOBODY 65534
+
+FIXTURE(preserve_tmpfs) {
+ char dir[32];
+ int luo_fd;
+ int session_fd;
+ bool mounted;
+};
+
+FIXTURE_SETUP(preserve_tmpfs)
+{
+ self->luo_fd = -1;
+ self->session_fd = -1;
+ self->mounted = false;
+
+ self->luo_fd = open(LIVEUPDATE_DEV, O_RDWR);
+ if (self->luo_fd < 0 && errno == ENOENT)
+ SKIP(return, "%s does not exist", LIVEUPDATE_DEV);
+ ASSERT_GE(self->luo_fd, 0);
+
+ self->session_fd = luo_create_session(self->luo_fd, "preserve-tmpfs");
+ ASSERT_GE(self->session_fd, 0);
+
+ strcpy(self->dir, "/tmp/luo-tmpfs-XXXXXX");
+ ASSERT_NE(mkdtemp(self->dir), NULL);
+ if (mount("tmpfs", self->dir, "tmpfs", 0, "size=1M"))
+ SKIP(return, "Failed to mount tmpfs: %s", strerror(errno));
+ self->mounted = true;
+ ASSERT_EQ(chdir(self->dir), 0);
+}
+
+FIXTURE_TEARDOWN(preserve_tmpfs)
+{
+ if (self->session_fd >= 0)
+ close(self->session_fd);
+ if (self->luo_fd >= 0)
+ close(self->luo_fd);
+ if (chdir("/"))
+ return;
+ if (self->mounted)
+ umount2(self->dir, MNT_DETACH);
+ rmdir(self->dir);
+}
+
+/* Create the file @path with some contents and the mode @mode. */
+static int create_file(const char *path, mode_t mode)
+{
+ int fd;
+
+ fd = open(path, O_CREAT | O_EXCL | O_RDWR, mode);
+ if (fd < 0)
+ return -1;
+
+ if (write(fd, "data", 4) != 4 || fchmod(fd, mode)) {
+ close(fd);
+ return -1;
+ }
+
+ return fd;
+}
+
+/* Open @path with @flags and preserve it. Return 0 or -errno. */
+static int preserve_path(int session_fd, const char *path, int flags,
+ __u64 token)
+{
+ int fd, ret;
+
+ fd = open(path, flags);
+ if (fd < 0)
+ return -errno;
+
+ ret = luo_session_preserve_fd(session_fd, fd, token);
+ close(fd);
+ return ret;
+}
+
+/* Like preserve_path(), but as the user nobody. */
+static int preserve_as_nobody(int session_fd, const char *path, __u64 token)
+{
+ int status;
+ pid_t pid;
+
+ pid = fork();
+ if (pid < 0)
+ return -errno;
+
+ if (!pid) {
+ if (setgroups(0, NULL) || setresgid(NOBODY, NOBODY, NOBODY) ||
+ setresuid(NOBODY, NOBODY, NOBODY))
+ _exit(255);
+ _exit(-preserve_path(session_fd, path, O_RDONLY, token));
+ }
+
+ if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
+ return -ECHILD;
+
+ return -WEXITSTATUS(status);
+}
+
+/* Add the inode flag @flag, e.g. FS_IMMUTABLE_FL, to the file @fd. */
+static int add_inode_flag(int fd, int flag)
+{
+ int flags;
+
+ if (ioctl(fd, FS_IOC_GETFLAGS, &flags))
+ return -errno;
+
+ flags |= flag;
+ if (ioctl(fd, FS_IOC_SETFLAGS, &flags))
+ return -errno;
+
+ return 0;
+}
+
+/*
+ * Test Case: Preserve Through Read-Only and Write-Only Fds
+ *
+ * A file can be preserved through a read-only fd, but not through a
+ * write-only fd, since the retrieved file can be read.
+ */
+TEST_F(preserve_tmpfs, access_mode)
+{
+ int fd;
+
+ fd = create_file("file", 0644);
+ ASSERT_GE(fd, 0);
+ close(fd);
+
+ EXPECT_EQ(preserve_path(self->session_fd, "file", O_WRONLY, 1), -EBADF);
+ EXPECT_EQ(preserve_path(self->session_fd, "file", O_RDONLY, 2), 0);
+}
+
+/*
+ * Test Case: Preserve Without Write Permission
+ *
+ * Preserving a file freezes its size for all of its users, so it requires
+ * permission to write to the file.
+ */
+TEST_F(preserve_tmpfs, unwritable)
+{
+ int fd;
+
+ fd = create_file("file", 0644);
+ ASSERT_GE(fd, 0);
+ close(fd);
+
+ EXPECT_EQ(preserve_as_nobody(self->session_fd, "file", 1), -EACCES);
+ EXPECT_EQ(preserve_path(self->session_fd, "file", O_RDONLY, 2), 0);
+}
+
+/*
+ * Test Case: Preserve an Append-Only File
+ *
+ * Freezing the size of an append-only file would stop all writes to it, so
+ * append-only files can't be preserved.
+ */
+TEST_F(preserve_tmpfs, append_only)
+{
+ int fd, ret;
+
+ fd = create_file("file", 0644);
+ ASSERT_GE(fd, 0);
+ ret = add_inode_flag(fd, FS_APPEND_FL);
+ close(fd);
+ if (ret == -ENOTTY || ret == -EOPNOTSUPP)
+ SKIP(return, "FS_IOC_SETFLAGS is not supported");
+ ASSERT_EQ(ret, 0);
+
+ EXPECT_EQ(preserve_path(self->session_fd, "file", O_RDONLY, 1), -EPERM);
+}
+
+/*
+ * Test Case: Preserve an Immutable File
+ *
+ * The size of an immutable file can't change anyway, so its owner may preserve
+ * it. Others still need permission to write to it.
+ */
+TEST_F(preserve_tmpfs, immutable)
+{
+ int fd, ret;
+
+ fd = create_file("own", 0644);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(fchown(fd, NOBODY, NOBODY), 0);
+ ret = add_inode_flag(fd, FS_IMMUTABLE_FL);
+ close(fd);
+ if (ret == -ENOTTY || ret == -EOPNOTSUPP)
+ SKIP(return, "FS_IOC_SETFLAGS is not supported");
+ ASSERT_EQ(ret, 0);
+
+ fd = create_file("other", 0666);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(add_inode_flag(fd, FS_IMMUTABLE_FL), 0);
+ close(fd);
+
+ EXPECT_EQ(preserve_as_nobody(self->session_fd, "own", 1), 0);
+ EXPECT_EQ(preserve_as_nobody(self->session_fd, "other", 2), -EPERM);
+}
+
+/*
+ * Test Case: Preserve Through a Read-Only Mount
+ *
+ * Preserving a file only needs permission to write to the file, not a writable
+ * mount, so it works through a read-only mount.
+ */
+TEST_F(preserve_tmpfs, rdonly_mount)
+{
+ int fd;
+
+ ASSERT_EQ(mkdir("src", 0755), 0);
+ fd = create_file("src/file", 0644);
+ ASSERT_GE(fd, 0);
+ close(fd);
+
+ ASSERT_EQ(mkdir("ro", 0755), 0);
+ ASSERT_EQ(mount("src", "ro", NULL, MS_BIND, NULL), 0);
+ ASSERT_EQ(mount(NULL, "ro", NULL, MS_REMOUNT | MS_BIND | MS_RDONLY,
+ NULL), 0);
+
+ EXPECT_EQ(preserve_path(self->session_fd, "ro/file", O_RDONLY, 1), 0);
+ umount2("ro", MNT_DETACH);
+}
+
+/*
+ * Test Case: Preserve on a Read-Only Superblock
+ *
+ * The size of a file on a read-only superblock can't change anyway, so its
+ * owner may preserve it. Others still need permission to write to it.
+ */
+TEST_F(preserve_tmpfs, rdonly_sb)
+{
+ int fd;
+
+ fd = create_file("own", 0644);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(fchown(fd, NOBODY, NOBODY), 0);
+ close(fd);
+
+ fd = create_file("other", 0666);
+ ASSERT_GE(fd, 0);
+ close(fd);
+
+ ASSERT_EQ(mount(NULL, self->dir, NULL, MS_REMOUNT | MS_RDONLY, NULL),
+ 0);
+
+ EXPECT_EQ(preserve_as_nobody(self->session_fd, "own", 1), 0);
+ EXPECT_EQ(preserve_as_nobody(self->session_fd, "other", 2), -EROFS);
+}
+
+/*
+ * Test Case: Frozen Size of a Preserved File
+ *
+ * While a file is preserved, its size can't change and no holes can be punched
+ * into it. Writes within the file still work. Closing the session unpreserves
+ * the file, after which its size can change again.
+ */
+TEST_F(preserve_tmpfs, frozen_size)
+{
+ long page_size = getpagesize();
+ struct stat st;
+ char c;
+ int fd;
+
+ fd = create_file("file", 0644);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(ftruncate(fd, 2 * page_size), 0);
+ ASSERT_EQ(luo_session_preserve_fd(self->session_fd, fd, 1), 0);
+
+ EXPECT_EQ(ftruncate(fd, page_size), -1);
+ EXPECT_EQ(errno, EPERM);
+ EXPECT_EQ(ftruncate(fd, 4 * page_size), -1);
+ EXPECT_EQ(errno, EPERM);
+ EXPECT_EQ(fallocate(fd, 0, 0, 4 * page_size), -1);
+ EXPECT_EQ(errno, EPERM);
+ EXPECT_EQ(fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, 0,
+ page_size), -1);
+ EXPECT_EQ(errno, EPERM);
+ EXPECT_EQ(pwrite(fd, "x", 1, 2 * page_size), -1);
+ EXPECT_EQ(errno, EPERM);
+
+ /* The size didn't change, but writes within the file work. */
+ ASSERT_EQ(fstat(fd, &st), 0);
+ EXPECT_EQ(st.st_size, 2 * page_size);
+ EXPECT_EQ(pwrite(fd, "x", 1, page_size), 1);
+ EXPECT_EQ(pread(fd, &c, 1, page_size), 1);
+ EXPECT_EQ(c, 'x');
+
+ ASSERT_EQ(close(self->session_fd), 0);
+ self->session_fd = -1;
+ EXPECT_EQ(ftruncate(fd, page_size), 0);
+
+ close(fd);
+}
+
TEST_HARNESS_MAIN
--
2.56.0.542.g3ef89b7020-goog